DLMS-SPODES-client 0.19.22__py3-none-any.whl → 0.19.24__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- DLMS_SPODES_client/FCS16.py +39 -39
- DLMS_SPODES_client/__init__.py +12 -12
- DLMS_SPODES_client/client.py +2091 -2091
- DLMS_SPODES_client/gurux_common/enums/TraceLevel.py +21 -21
- DLMS_SPODES_client/gurux_dlms/AesGcmParameter.py +37 -37
- DLMS_SPODES_client/gurux_dlms/CountType.py +16 -16
- DLMS_SPODES_client/gurux_dlms/GXByteBuffer.py +545 -545
- DLMS_SPODES_client/gurux_dlms/GXCiphering.py +196 -196
- DLMS_SPODES_client/gurux_dlms/GXDLMS.py +426 -426
- DLMS_SPODES_client/gurux_dlms/GXDLMSChippering.py +237 -237
- DLMS_SPODES_client/gurux_dlms/GXDLMSChipperingStream.py +977 -977
- DLMS_SPODES_client/gurux_dlms/GXDLMSConfirmedServiceError.py +90 -90
- DLMS_SPODES_client/gurux_dlms/GXDLMSException.py +139 -139
- DLMS_SPODES_client/gurux_dlms/GXDLMSLNParameters.py +33 -33
- DLMS_SPODES_client/gurux_dlms/GXDLMSSNParameters.py +21 -21
- DLMS_SPODES_client/gurux_dlms/GXDLMSSettings.py +254 -254
- DLMS_SPODES_client/gurux_dlms/GXReplyData.py +87 -87
- DLMS_SPODES_client/gurux_dlms/HdlcControlFrame.py +9 -9
- DLMS_SPODES_client/gurux_dlms/MBusCommand.py +8 -8
- DLMS_SPODES_client/gurux_dlms/MBusEncryptionMode.py +27 -27
- DLMS_SPODES_client/gurux_dlms/ResponseType.py +8 -8
- DLMS_SPODES_client/gurux_dlms/SetResponseType.py +29 -29
- DLMS_SPODES_client/gurux_dlms/_HDLCInfo.py +9 -9
- DLMS_SPODES_client/gurux_dlms/__init__.py +75 -75
- DLMS_SPODES_client/gurux_dlms/enums/Access.py +12 -12
- DLMS_SPODES_client/gurux_dlms/enums/ApplicationReference.py +14 -14
- DLMS_SPODES_client/gurux_dlms/enums/Authentication.py +41 -41
- DLMS_SPODES_client/gurux_dlms/enums/BerType.py +35 -35
- DLMS_SPODES_client/gurux_dlms/enums/Command.py +285 -285
- DLMS_SPODES_client/gurux_dlms/enums/Definition.py +9 -9
- DLMS_SPODES_client/gurux_dlms/enums/ErrorCode.py +46 -46
- DLMS_SPODES_client/gurux_dlms/enums/ExceptionServiceError.py +12 -12
- DLMS_SPODES_client/gurux_dlms/enums/HardwareResource.py +10 -10
- DLMS_SPODES_client/gurux_dlms/enums/HdlcFrameType.py +9 -9
- DLMS_SPODES_client/gurux_dlms/enums/Initiate.py +10 -10
- DLMS_SPODES_client/gurux_dlms/enums/LoadDataSet.py +13 -13
- DLMS_SPODES_client/gurux_dlms/enums/ObjectType.py +306 -306
- DLMS_SPODES_client/gurux_dlms/enums/Priority.py +7 -7
- DLMS_SPODES_client/gurux_dlms/enums/RequestTypes.py +9 -9
- DLMS_SPODES_client/gurux_dlms/enums/Security.py +14 -14
- DLMS_SPODES_client/gurux_dlms/enums/Service.py +16 -16
- DLMS_SPODES_client/gurux_dlms/enums/ServiceClass.py +9 -9
- DLMS_SPODES_client/gurux_dlms/enums/ServiceError.py +8 -8
- DLMS_SPODES_client/gurux_dlms/enums/Standard.py +18 -18
- DLMS_SPODES_client/gurux_dlms/enums/StateError.py +7 -7
- DLMS_SPODES_client/gurux_dlms/enums/Task.py +10 -10
- DLMS_SPODES_client/gurux_dlms/enums/VdeStateError.py +10 -10
- DLMS_SPODES_client/gurux_dlms/enums/__init__.py +33 -33
- DLMS_SPODES_client/gurux_dlms/internal/_GXCommon.py +1673 -1673
- DLMS_SPODES_client/logger.py +56 -56
- DLMS_SPODES_client/services.py +97 -97
- DLMS_SPODES_client/session.py +365 -365
- DLMS_SPODES_client/settings.py +48 -48
- DLMS_SPODES_client/task.py +1841 -1842
- {dlms_spodes_client-0.19.22.dist-info → dlms_spodes_client-0.19.24.dist-info}/METADATA +29 -27
- dlms_spodes_client-0.19.24.dist-info/RECORD +61 -0
- dlms_spodes_client-0.19.22.dist-info/RECORD +0 -61
- {dlms_spodes_client-0.19.22.dist-info → dlms_spodes_client-0.19.24.dist-info}/WHEEL +0 -0
- {dlms_spodes_client-0.19.22.dist-info → dlms_spodes_client-0.19.24.dist-info}/entry_points.txt +0 -0
- {dlms_spodes_client-0.19.22.dist-info → dlms_spodes_client-0.19.24.dist-info}/top_level.txt +0 -0
DLMS_SPODES_client/client.py
CHANGED
|
@@ -1,2091 +1,2091 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
from typing_extensions import deprecated
|
|
3
|
-
import dataclasses
|
|
4
|
-
import time
|
|
5
|
-
from abc import ABC, abstractmethod
|
|
6
|
-
from functools import cached_property, reduce
|
|
7
|
-
from struct import pack
|
|
8
|
-
from collections import deque
|
|
9
|
-
from itertools import count
|
|
10
|
-
from enum import IntEnum, auto, IntFlag
|
|
11
|
-
from typing import TextIO, Deque, Any, Callable, Optional
|
|
12
|
-
import threading
|
|
13
|
-
import datetime
|
|
14
|
-
import os
|
|
15
|
-
import hashlib
|
|
16
|
-
from Cryptodome.Cipher import AES
|
|
17
|
-
from StructResult import result
|
|
18
|
-
from DLMS_SPODES_communications import Network, Serial, RS485, BLEKPZ, base
|
|
19
|
-
from DLMS_SPODES.cosem_interface_classes import overview
|
|
20
|
-
from DLMS_SPODES.cosem_interface_classes.collection import Collection, InterfaceClass, ic, cdt, ut, Data, AssociationLN
|
|
21
|
-
from DLMS_SPODES.cosem_interface_classes.security_setup.ver1 import SecuritySuite
|
|
22
|
-
from DLMS_SPODES.enums import (
|
|
23
|
-
Transmit, Application, ActionRequest, ReadResponse, ServiceError, AssociationResult, SetRequest, ConfirmedServiceError, AARQapdu, ACSEAPDU, XDLMSAPDU,
|
|
24
|
-
VariableAccessSpecification, AcseServiceUser
|
|
25
|
-
)
|
|
26
|
-
from DLMS_SPODES.cosem_interface_classes.association_ln import mechanism_id, method
|
|
27
|
-
from DLMS_SPODES.cosem_interface_classes.association_ln.authentication_mechanism_name import AuthenticationMechanismName
|
|
28
|
-
from DLMS_SPODES.hdlc import frame, sub_layer
|
|
29
|
-
from DLMS_SPODES import pdu_enums as pdu, exceptions as exc
|
|
30
|
-
from DLMS_SPODES.types.implementations import enums, long_unsigneds, bitstrings, octet_string
|
|
31
|
-
from DLMSCommunicationProfile import communication_profile as c_pf, OSI
|
|
32
|
-
from .gurux_dlms import GXDLMSSettings, GXByteBuffer, GXReplyData, GXDLMSException
|
|
33
|
-
from .gurux_dlms.enums import Security, Standard, BerType, RequestTypes, Service
|
|
34
|
-
from .gurux_dlms.GXDLMS import GXDLMS
|
|
35
|
-
from .gurux_dlms.GXDLMSLNParameters import GXDLMSLNParameters
|
|
36
|
-
from .gurux_dlms.GXDLMSSNParameters import GXDLMSSNParameters
|
|
37
|
-
from .gurux_dlms.AesGcmParameter import AesGcmParameter
|
|
38
|
-
from .gurux_dlms.GXCiphering import GXCiphering
|
|
39
|
-
from .gurux_dlms.GXDLMSConfirmedServiceError import GXDLMSConfirmedServiceError
|
|
40
|
-
from .gurux_dlms.GXDLMSChippering import GXDLMSChippering
|
|
41
|
-
from .gurux_dlms import CountType
|
|
42
|
-
from .gurux_dlms.internal._GXCommon import _GXCommon
|
|
43
|
-
from .logger import logger, LogLevel as logL
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def copy_with_align(data: bytes, block_size: int = 16) -> bytes:
|
|
47
|
-
""" fill by zeros to full 16 bytes blocks """
|
|
48
|
-
return data + bytes((block_size - len(data) % block_size) % block_size)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
TZ = datetime.timezone(datetime.datetime.now() - datetime.datetime.utcnow())
|
|
52
|
-
""" os time zone """
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def get_os_datetime() -> datetime.datetime:
|
|
56
|
-
""" return os datetime with time zone """
|
|
57
|
-
return datetime.datetime.now(TZ)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def get_os_time() -> str:
|
|
61
|
-
""" return os time with time zone """
|
|
62
|
-
return get_os_datetime().strftime('%H:%M:%S')
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
class State(ABC):
|
|
66
|
-
|
|
67
|
-
@abstractmethod
|
|
68
|
-
def __str__(self):
|
|
69
|
-
""""""
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
@dataclasses.dataclass
|
|
73
|
-
class Text(State):
|
|
74
|
-
value: str
|
|
75
|
-
|
|
76
|
-
def __str__(self):
|
|
77
|
-
return self.value
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
class IDFactory:
|
|
81
|
-
def __init__(self, prefix: str):
|
|
82
|
-
self.count = count()
|
|
83
|
-
self.value = set()
|
|
84
|
-
self.prefix = prefix
|
|
85
|
-
|
|
86
|
-
def create(self) -> str:
|
|
87
|
-
id_ = F"{self.prefix}{next(self.count)}"
|
|
88
|
-
"""for identification before LDN reading"""
|
|
89
|
-
while True:
|
|
90
|
-
if id_ not in self.value:
|
|
91
|
-
self.register(id_)
|
|
92
|
-
return id_
|
|
93
|
-
else:
|
|
94
|
-
id_ = F"{self.prefix}{next(self.count)}"
|
|
95
|
-
|
|
96
|
-
def register(self, id_: str):
|
|
97
|
-
if id_ not in self.value:
|
|
98
|
-
self.value.add(id_)
|
|
99
|
-
else:
|
|
100
|
-
raise ValueError(F"error in register ID={id_}: already exist")
|
|
101
|
-
|
|
102
|
-
def remove(self, value: str) -> bool:
|
|
103
|
-
try:
|
|
104
|
-
self.value.remove(value)
|
|
105
|
-
return True
|
|
106
|
-
except KeyError:
|
|
107
|
-
return False
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
class Client:
|
|
111
|
-
id: str | None
|
|
112
|
-
name: str = "unknown"
|
|
113
|
-
com_profile: c_pf.CommunicationProfile
|
|
114
|
-
__del_cb: Callable[[str], bool] | None
|
|
115
|
-
__universal: bool
|
|
116
|
-
level: OSI
|
|
117
|
-
log_file: TextIO
|
|
118
|
-
media: base.Media | None
|
|
119
|
-
lock: asyncio.Lock
|
|
120
|
-
last_transfer_time: datetime.timedelta | None
|
|
121
|
-
connection_time_release: int
|
|
122
|
-
received_frames: Deque[frame.Frame]
|
|
123
|
-
current_obj: InterfaceClass | None
|
|
124
|
-
reply: GXReplyData
|
|
125
|
-
settings: GXDLMSSettings
|
|
126
|
-
__sap: enums.ClientSAP
|
|
127
|
-
secret: bytes
|
|
128
|
-
SA: frame.Address
|
|
129
|
-
DA: frame.Address
|
|
130
|
-
negotiated_conformance: bitstrings.Conformance
|
|
131
|
-
_objects: Optional[Collection]
|
|
132
|
-
APP_CONTEXT_NAME = cdt.OctetString("60857405080101")
|
|
133
|
-
"""AssociationLN.application_context_name a-xdr encode"""
|
|
134
|
-
DEF_DLMS_VER: int = 6
|
|
135
|
-
"""DLMS version by default"""
|
|
136
|
-
m_id: mechanism_id.MechanismIdElement
|
|
137
|
-
"""None is the AUTO from current association"""
|
|
138
|
-
addr_size: frame.AddressLength
|
|
139
|
-
logging_disable: bool
|
|
140
|
-
state: State
|
|
141
|
-
|
|
142
|
-
def __init__(self,
|
|
143
|
-
SAP: int = 0x10,
|
|
144
|
-
secret: str | bytes = "",
|
|
145
|
-
conformance: str = None,
|
|
146
|
-
addr_size: int = -1,
|
|
147
|
-
media: base.Media = None,
|
|
148
|
-
id_: str | int = None,
|
|
149
|
-
m_id: int = 0,
|
|
150
|
-
universal: bool = False,
|
|
151
|
-
del_cb: Callable[[str], bool] = None,
|
|
152
|
-
com_profile: c_pf.CommunicationProfile = None):
|
|
153
|
-
self.com_profile = c_pf.HDLC() if com_profile is None else com_profile
|
|
154
|
-
"""communication profile"""
|
|
155
|
-
self.id = id_
|
|
156
|
-
"""for identification before LDN reading"""
|
|
157
|
-
self.__universal = universal
|
|
158
|
-
"""matching LDN if True else change server Type"""
|
|
159
|
-
self.__del_cb = del_cb
|
|
160
|
-
"""callback to unregister id"""
|
|
161
|
-
self.logging_disable = False
|
|
162
|
-
"""turn off logging by default"""
|
|
163
|
-
self._objects = None
|
|
164
|
-
self.__sap = enums.ClientSAP(SAP)
|
|
165
|
-
"""Service Access Point. Default <Public>"""
|
|
166
|
-
self.media = Serial(port="COM3") if media is None else media
|
|
167
|
-
""" physical layer """
|
|
168
|
-
if com_profile is None:
|
|
169
|
-
self.com_profile = c_pf.HDLC()
|
|
170
|
-
self.server_SAP = long_unsigneds.ServerSAP(1)
|
|
171
|
-
if isinstance(secret, str):
|
|
172
|
-
self.secret = bytes.fromhex(secret)
|
|
173
|
-
elif isinstance(secret, bytes):
|
|
174
|
-
self.secret = secret
|
|
175
|
-
self.protocol_version = cdt.BitString('1') # max 8 bit
|
|
176
|
-
""" Protocol Version of the AARQ APDU """
|
|
177
|
-
# TODO: REMOVE IT BULLSHIT
|
|
178
|
-
self.invocationCounter = '0.0.43.1.0.255'
|
|
179
|
-
self.lock = asyncio.Lock()
|
|
180
|
-
""" lock for exchange access to device """
|
|
181
|
-
self.addr_size = frame.AddressLength(addr_size)
|
|
182
|
-
"""server address size, -1 is AUTO"""
|
|
183
|
-
self.m_id = mechanism_id.MechanismIdElement(m_id)
|
|
184
|
-
# from AssociationLN.xDLMSinfo
|
|
185
|
-
self.quality_of_service = 0
|
|
186
|
-
self.receive_pdu_size = 0xffff # max available
|
|
187
|
-
self.proposed_conformance = bitstrings.Conformance(conformance)
|
|
188
|
-
self.negotiated_conformance = self.proposed_conformance.copy()
|
|
189
|
-
|
|
190
|
-
self.last_transfer_time = None
|
|
191
|
-
""" decided time transfer from server to client """
|
|
192
|
-
|
|
193
|
-
self.connection_time_release = 10
|
|
194
|
-
""" number of second for port release after inactivity """
|
|
195
|
-
|
|
196
|
-
self.received_frames = deque()
|
|
197
|
-
""" HDLC frames container from server """
|
|
198
|
-
|
|
199
|
-
self.send_frames = deque()
|
|
200
|
-
self.level = OSI.NONE
|
|
201
|
-
"""OSI level"""
|
|
202
|
-
self.settings = GXDLMSSettings(False)
|
|
203
|
-
|
|
204
|
-
self.current_obj = None
|
|
205
|
-
""" current transferring object. For progress bar now """
|
|
206
|
-
|
|
207
|
-
# from Gurux Client
|
|
208
|
-
self.use_protected_release = False
|
|
209
|
-
""" Gurux Client: If protected release is used release is including a ciphered xDLMS Initiate request. """
|
|
210
|
-
|
|
211
|
-
self.state = Text("undefined")
|
|
212
|
-
|
|
213
|
-
@property
|
|
214
|
-
def objects(self) -> Collection:
|
|
215
|
-
if self._objects is None:
|
|
216
|
-
raise exc.DLMSException("client hasn't objects")
|
|
217
|
-
return self._objects
|
|
218
|
-
|
|
219
|
-
def __del__(self):
|
|
220
|
-
if self.__del_cb:
|
|
221
|
-
self.__del_cb(self.id)
|
|
222
|
-
|
|
223
|
-
def is_universal(self) -> bool:
|
|
224
|
-
return self.__universal
|
|
225
|
-
|
|
226
|
-
def log(self, level: logL, msg: str | State):
|
|
227
|
-
"""use logger with level and extra=LDN"""
|
|
228
|
-
if not self.logging_disable:
|
|
229
|
-
logger.log(level=level,
|
|
230
|
-
msg=str(msg),
|
|
231
|
-
extra={"id": self._objects.LDN.value.to_str() if (self._objects and self._objects.LDN.value) else F"{self.id}"})
|
|
232
|
-
if level == logL.STATE and isinstance(msg, State):
|
|
233
|
-
self.state = msg
|
|
234
|
-
|
|
235
|
-
@property
|
|
236
|
-
def SAP(self) -> enums.ClientSAP:
|
|
237
|
-
return self.__sap
|
|
238
|
-
|
|
239
|
-
@SAP.setter
|
|
240
|
-
def SAP(self, value):
|
|
241
|
-
"""change SAP if associationLN possible"""
|
|
242
|
-
new_SAP = enums.ClientSAP(value)
|
|
243
|
-
if self._objects is not None:
|
|
244
|
-
self._objects.sap2association(new_SAP)
|
|
245
|
-
else:
|
|
246
|
-
"""OK"""
|
|
247
|
-
self.__sap.set(value)
|
|
248
|
-
|
|
249
|
-
def get_ass_id(self) -> int:
|
|
250
|
-
"""return current Association ID"""
|
|
251
|
-
return int(self.current_association.logical_name.e)
|
|
252
|
-
|
|
253
|
-
def get_channel_index(self) -> int:
|
|
254
|
-
"""todo: remove in future. get communication channel by media"""
|
|
255
|
-
match self.media:
|
|
256
|
-
case Serial(): return 0
|
|
257
|
-
case RS485(): return 1
|
|
258
|
-
case Network(): return 2
|
|
259
|
-
case BLEKPZ(): return 3
|
|
260
|
-
case _: raise ValueError(F"can't calculate channel index by media: {self.media}")
|
|
261
|
-
|
|
262
|
-
def get_frame(self, read_data: bytearray, reply: GXReplyData) -> frame.Frame | None:
|
|
263
|
-
reply.complete = False
|
|
264
|
-
while len(read_data) != 0:
|
|
265
|
-
new_frame = frame.Frame.try_from(read_data)
|
|
266
|
-
if not isinstance(new_frame, frame.Frame):
|
|
267
|
-
return None
|
|
268
|
-
reply.complete = True
|
|
269
|
-
if new_frame.is_for_me(self.DA, self.SA):
|
|
270
|
-
self.received_frames.append(new_frame)
|
|
271
|
-
if new_frame.is_segmentation:
|
|
272
|
-
reply.moreData |= RequestTypes.FRAME
|
|
273
|
-
else:
|
|
274
|
-
reply.moreData &= ~RequestTypes.FRAME
|
|
275
|
-
# check control TODO: rewrite it
|
|
276
|
-
if new_frame.control.is_unnumbered():
|
|
277
|
-
if new_frame.control in (frame.Control.UA_F, frame.Control.SNRM_P):
|
|
278
|
-
self.settings.resetFrameSequence()
|
|
279
|
-
return new_frame
|
|
280
|
-
elif new_frame.control == frame.Control.UI_PF:
|
|
281
|
-
self.log(logL.WARN, """ TODO: Here Notify handler """)
|
|
282
|
-
else:
|
|
283
|
-
self.log(logL.INFO, F'Can\'t processing HDLC Frame: {new_frame.control}')
|
|
284
|
-
elif new_frame.control.is_supervisory():
|
|
285
|
-
self.settings.receiverFrame = frame.Control.next_receiver_sequence(self.settings.receiverFrame)
|
|
286
|
-
return new_frame
|
|
287
|
-
elif self.settings.senderFrame.is_info():
|
|
288
|
-
expected = frame.Control.next_receiver_sequence(frame.Control.next_send_sequence(self.settings.receiverFrame))
|
|
289
|
-
if new_frame.control == expected:
|
|
290
|
-
self.settings.receiverFrame = new_frame.control
|
|
291
|
-
return new_frame
|
|
292
|
-
else:
|
|
293
|
-
self.log(logL.INFO, F'Invalid HDLC Frame: {new_frame.control} Expected: {expected}')
|
|
294
|
-
else:
|
|
295
|
-
expected = frame.Control.next_send_sequence(self.settings.receiverFrame)
|
|
296
|
-
# If answer for RR.
|
|
297
|
-
if new_frame.control == expected:
|
|
298
|
-
self.settings.receiverFrame = new_frame.control
|
|
299
|
-
return new_frame
|
|
300
|
-
else:
|
|
301
|
-
self.log(logL.INFO, F'Invalid HDLC Frame: {new_frame.control} Expected: {expected}')
|
|
302
|
-
self.log(logL.WARN, F"Drop frame {new_frame}")
|
|
303
|
-
else:
|
|
304
|
-
self.log(logL.WARN, F"ALIEN frame {new_frame}, expect with SA:{self.SA}")
|
|
305
|
-
# FROM GURUX - if new_frame.control == frame.Control.UI_PF: # search next frame in read_data
|
|
306
|
-
|
|
307
|
-
def handleGbt(self, reply: GXReplyData) -> result.Ok | result.Error:
|
|
308
|
-
index = reply.data.position - 1
|
|
309
|
-
reply.windowSize = self.settings.windowSize
|
|
310
|
-
bc = reply.data.getUInt8()
|
|
311
|
-
reply.streaming = (bc & 0x40) != 0
|
|
312
|
-
windowSize = int(bc & 0x3F)
|
|
313
|
-
bn = reply.data.getUInt16()
|
|
314
|
-
bna = reply.data.getUInt16()
|
|
315
|
-
reply.blockNumber = bn
|
|
316
|
-
reply.blockNumberAck = bna
|
|
317
|
-
self.settings.blockNumberAck = reply.blockNumber
|
|
318
|
-
reply.command = None
|
|
319
|
-
len_ = _GXCommon.getObjectCount(reply.data)
|
|
320
|
-
if len_ > reply.data.size - reply.data.position:
|
|
321
|
-
reply.complete = False
|
|
322
|
-
return result.Error.from_e(RuntimeError("not enouth reply data size"))
|
|
323
|
-
GXDLMS.getDataFromBlock(reply.data, index)
|
|
324
|
-
if (bc & 0x80) == 0:
|
|
325
|
-
reply.moreData = (RequestTypes(reply.moreData | RequestTypes.GBT))
|
|
326
|
-
else:
|
|
327
|
-
reply.moreData = (RequestTypes(reply.moreData & ~RequestTypes.GBT))
|
|
328
|
-
if reply.data.size != 0:
|
|
329
|
-
reply.data.position = 0
|
|
330
|
-
if isinstance(res_pdu := self.getPdu(), result.Error):
|
|
331
|
-
return res_pdu.with_msg("handle GBT")
|
|
332
|
-
# if reply.data.position != reply.data.size and (reply.command == XDLMSAPDU.READ_RESPONSE or reply.command == XDLMSAPDU.GET_RESPONSE) and (reply.moreData == RequestTypes.NONE or reply.peek):
|
|
333
|
-
# reply.data.position = 0
|
|
334
|
-
# cls.getValueFromData(settings, reply)
|
|
335
|
-
return result.OK
|
|
336
|
-
|
|
337
|
-
def getPdu(self, reply: GXReplyData) -> result.Ok | result.Error:
|
|
338
|
-
# TODO: make return pdu
|
|
339
|
-
if reply.command is None:
|
|
340
|
-
if reply.data.size - reply.data.position == 0:
|
|
341
|
-
return result.Error(ValueError("Invalid PDU"), "getpdu")
|
|
342
|
-
index = reply.data.position
|
|
343
|
-
reply.command = XDLMSAPDU(reply.data.getUInt8())
|
|
344
|
-
match reply.command:
|
|
345
|
-
case XDLMSAPDU.GET_RESPONSE:
|
|
346
|
-
response_type: int = reply.data.getUInt8()
|
|
347
|
-
invoke_id_and_priority = reply.data.getUInt8() # TODO: matching with setting params
|
|
348
|
-
match response_type:
|
|
349
|
-
case pdu.GetResponse.NORMAL:
|
|
350
|
-
match reply.data.getUInt8(): # Get-Data-Result[0]
|
|
351
|
-
case 0:
|
|
352
|
-
GXDLMS.getDataFromBlock(reply.data, 0)
|
|
353
|
-
case 1:
|
|
354
|
-
reply.error = pdu.DataAccessResult(reply.data.getUInt8())
|
|
355
|
-
if reply.error != 0:
|
|
356
|
-
return result.Error.from_e(exc.ResultError(reply.error), "get pdu")
|
|
357
|
-
case err:
|
|
358
|
-
return result.Error.from_e(ValueError(F'Got Get-Data-Result[0] {err}, expect 0 or 1'), "get pdu")
|
|
359
|
-
GXDLMS.getDataFromBlock(reply.data, 0)
|
|
360
|
-
case pdu.GetResponse.WITH_DATABLOCK:
|
|
361
|
-
last_block = reply.data.getUInt8()
|
|
362
|
-
if last_block == 0:
|
|
363
|
-
reply.moreData |= RequestTypes.DATABLOCK
|
|
364
|
-
else:
|
|
365
|
-
reply.moreData &= ~RequestTypes.DATABLOCK
|
|
366
|
-
block_number = reply.data.getUInt32()
|
|
367
|
-
if block_number == 0 and self.settings.blockIndex == 1: # if start block_index == 0
|
|
368
|
-
self.settings.setBlockIndex(0)
|
|
369
|
-
if block_number != self.settings.blockIndex:
|
|
370
|
-
return result.Error.from_e(ValueError(F"Invalid Block number. It is {block_number} and it should be {self.settings.blockIndex}."), "get pdu")
|
|
371
|
-
match reply.data.getUInt8(): # DataBlock-G.result,
|
|
372
|
-
case 0:
|
|
373
|
-
if reply.data.position != len(reply.data):
|
|
374
|
-
block_length = _GXCommon.getObjectCount(reply.data)
|
|
375
|
-
if (reply.moreData & RequestTypes.FRAME) == 0:
|
|
376
|
-
if block_length > len(reply.data) - reply.data.position:
|
|
377
|
-
return result.Error.from_e(ValueError("Invalid block length."), "get pdu")
|
|
378
|
-
reply.command = None
|
|
379
|
-
if block_length == 0:
|
|
380
|
-
reply.data.size = index
|
|
381
|
-
else:
|
|
382
|
-
GXDLMS.getDataFromBlock(reply.data, index)
|
|
383
|
-
if reply.moreData == RequestTypes.NONE:
|
|
384
|
-
if not reply.peek:
|
|
385
|
-
reply.data.position = 0
|
|
386
|
-
self.settings.resetBlockIndex()
|
|
387
|
-
if reply.moreData == RequestTypes.NONE and self.settings and self.settings.command == XDLMSAPDU.GET_REQUEST \
|
|
388
|
-
and self.settings.commandType == pdu.GetResponse.WITH_LIST:
|
|
389
|
-
GXDLMS.handleGetResponseWithList(self.settings, reply)
|
|
390
|
-
return result.OK
|
|
391
|
-
case 1:
|
|
392
|
-
reply.error = pdu.DataAccessResult(reply.data.getUInt8())
|
|
393
|
-
if reply.error != 0:
|
|
394
|
-
return result.Error.from_e(exc.ResultError(reply.error), "get pdu")
|
|
395
|
-
case err:
|
|
396
|
-
return result.Error.from_e(ValueError(F'Got DataBlock-G.result {err}, expect 0 or 1'), "get pdu")
|
|
397
|
-
case pdu.GetResponse.WITH_LIST:
|
|
398
|
-
GXDLMS.handleGetResponseWithList(self.settings, reply)
|
|
399
|
-
return result.OK
|
|
400
|
-
case err:
|
|
401
|
-
return result.Error.from_e(ValueError(F"Got Invalid Get response {err}, expect {', '.join(map(lambda it: F'{it.name} = {it.value}', pdu.GetResponse))}"), "get pdu")
|
|
402
|
-
case XDLMSAPDU.READ_RESPONSE:
|
|
403
|
-
if not GXDLMS.handleReadResponse(self.settings, reply, index):
|
|
404
|
-
return result.OK
|
|
405
|
-
case XDLMSAPDU.SET_RESPONSE:
|
|
406
|
-
response_type: int = reply.data.getUInt8()
|
|
407
|
-
invoke_id_and_priority = reply.data.getUInt8() # TODO: matching with setting params
|
|
408
|
-
match response_type:
|
|
409
|
-
case pdu.SetResponse.NORMAL:
|
|
410
|
-
reply.error = pdu.DataAccessResult(reply.data.getUInt8())
|
|
411
|
-
if reply.error != 0:
|
|
412
|
-
return result.Error.from_e(exc.ResultError(reply.error), "get pdu")
|
|
413
|
-
case pdu.SetResponse.DATABLOCK:
|
|
414
|
-
block_number = reply.data.getUInt32()
|
|
415
|
-
case pdu.SetResponse.LAST_DATABLOCK:
|
|
416
|
-
reply.error = pdu.DataAccessResult(reply.data.getUInt8())
|
|
417
|
-
if reply.error != 0:
|
|
418
|
-
return result.Error.from_e(exc.ResultError(reply.error), "get pdu")
|
|
419
|
-
block_number = reply.data.getUInt32()
|
|
420
|
-
case pdu.SetResponse.LAST_DATABLOCK_WITH_LIST:
|
|
421
|
-
raise RuntimeError("Not released in Client")
|
|
422
|
-
case pdu.SetResponse.WITH_LIST:
|
|
423
|
-
cnt = _GXCommon.getObjectCount(reply.data)
|
|
424
|
-
pos = 0
|
|
425
|
-
while pos != cnt:
|
|
426
|
-
reply.error = pdu.DataAccessResult(reply.data.getUInt8())
|
|
427
|
-
if reply.error != 0:
|
|
428
|
-
return result.Error.from_e(exc.ResultError(reply.error), "get pdu")
|
|
429
|
-
pos += 1
|
|
430
|
-
case err:
|
|
431
|
-
return result.Error.from_e(ValueError(F"Got Invalid Set response {err}, expect {', '.join(map(lambda it: F'{it.name} = {it.value}', pdu.SetResponse))}"), "get pdu")
|
|
432
|
-
case XDLMSAPDU.WRITE_RESPONSE:
|
|
433
|
-
cnt = _GXCommon.getObjectCount(reply.data)
|
|
434
|
-
pos = 0
|
|
435
|
-
while pos != cnt:
|
|
436
|
-
ret = reply.data.getUInt8()
|
|
437
|
-
if ret != 0:
|
|
438
|
-
reply.error = reply.data.getUInt8()
|
|
439
|
-
pos += 1
|
|
440
|
-
case XDLMSAPDU.ACTION_RESPONSE:
|
|
441
|
-
action_response = reply.data.getUInt8()
|
|
442
|
-
invoke_id_and_priority = reply.data.getUInt8()
|
|
443
|
-
match action_response:
|
|
444
|
-
case pdu.ActionResponse.NORMAL:
|
|
445
|
-
reply.error = pdu.ActionResult(reply.data.getUInt8())
|
|
446
|
-
if reply.error != 0:
|
|
447
|
-
return result.Error(exc.ResultError(reply.error), "get pdu")
|
|
448
|
-
if reply.data.position < reply.data.size:
|
|
449
|
-
ret = reply.data.getUInt8()
|
|
450
|
-
if ret == 0:
|
|
451
|
-
GXDLMS.getDataFromBlock(reply.data, 0)
|
|
452
|
-
elif ret == 1:
|
|
453
|
-
ret = int(reply.data.getUInt8())
|
|
454
|
-
if ret != 0:
|
|
455
|
-
reply.error = reply.data.getUInt8()
|
|
456
|
-
if ret == 9 and reply.error == 16:
|
|
457
|
-
reply.data.position = reply.data.position - 2
|
|
458
|
-
GXDLMS.getDataFromBlock(reply.data, 0)
|
|
459
|
-
reply.error = 0
|
|
460
|
-
ret = 0
|
|
461
|
-
else:
|
|
462
|
-
GXDLMS.getDataFromBlock(reply.data, 0)
|
|
463
|
-
else:
|
|
464
|
-
return result.Error.from_e(Exception("HandleActionResponseNormal failed. " + "Invalid tag."), "get pdu")
|
|
465
|
-
case pdu.ActionResponse.WITH_PBLOCK:
|
|
466
|
-
raise RuntimeError("Not released in Client")
|
|
467
|
-
case pdu.ActionResponse.WITH_LIST:
|
|
468
|
-
raise RuntimeError("Not released in Client")
|
|
469
|
-
case pdu.ActionResponse.NEXT_PBLOCK:
|
|
470
|
-
raise RuntimeError("Not released in Client")
|
|
471
|
-
case err:
|
|
472
|
-
return result.Error.from_e(ValueError(F"got {pdu.ActionResponse}: {err}, expect {', '.join(map(lambda it: F'{it.name} = {it.value}', pdu.ActionResponse))}"), "get pdu")
|
|
473
|
-
case XDLMSAPDU.ACCESS_RESPONSE:
|
|
474
|
-
data = reply.data
|
|
475
|
-
invokeId = reply.data.getUInt32()
|
|
476
|
-
len_ = reply.data.getUInt8()
|
|
477
|
-
tmp = None
|
|
478
|
-
if len_ != 0:
|
|
479
|
-
tmp = bytearray(len_)
|
|
480
|
-
data.get(tmp)
|
|
481
|
-
reply.time = _GXCommon.changeType(self.settings, tmp, DataType.DATETIME)
|
|
482
|
-
data.getUInt8()
|
|
483
|
-
case XDLMSAPDU.GENERAL_BLOCK_TRANSFER:
|
|
484
|
-
if not self.settings.isServer and (reply.moreData & RequestTypes.FRAME) == 0:
|
|
485
|
-
if isinstance(res_gbt := self.handleGbt(reply), result.Error):
|
|
486
|
-
return res_gbt
|
|
487
|
-
case ACSEAPDU.AARQ | ACSEAPDU.AARE:
|
|
488
|
-
# This is parsed later.
|
|
489
|
-
reply.data.position = reply.data.position - 1
|
|
490
|
-
case ACSEAPDU.RLRE | ACSEAPDU.RLRQ:
|
|
491
|
-
pass
|
|
492
|
-
case XDLMSAPDU.CONFIRMED_SERVICE_ERROR:
|
|
493
|
-
GXDLMS.handleConfirmedServiceError(reply)
|
|
494
|
-
case XDLMSAPDU.EXCEPTION_RESPONSE:
|
|
495
|
-
GXDLMS.handleExceptionResponse(reply)
|
|
496
|
-
case XDLMSAPDU.GET_REQUEST | XDLMSAPDU.READ_REQUEST | XDLMSAPDU.WRITE_REQUEST | XDLMSAPDU.SET_REQUEST | XDLMSAPDU.ACTION_REQUEST:
|
|
497
|
-
pass
|
|
498
|
-
case XDLMSAPDU.GLO_READ_REQUEST | XDLMSAPDU.GLO_WRITE_REQUEST | XDLMSAPDU.GLO_GET_REQUEST | XDLMSAPDU.GLO_SET_REQUEST | XDLMSAPDU.GLO_ACTION_REQUEST | \
|
|
499
|
-
XDLMSAPDU.DED_GET_REQUEST | XDLMSAPDU.DED_SET_REQUEST | XDLMSAPDU.DED_ACTION_REQUEST:
|
|
500
|
-
if self.settings.cipher is None:
|
|
501
|
-
return result.Error.from_e(ServiceError("Secure connection is not supported."), "get pdu")
|
|
502
|
-
if (reply.moreData & RequestTypes.FRAME) == 0:
|
|
503
|
-
reply.data.position = reply.data.position - 1
|
|
504
|
-
p = None
|
|
505
|
-
if self.settings.cipher.dedicatedKey and (OSI.APPLICATION in self.level):
|
|
506
|
-
p = AesGcmParameter(self.settings.sourceSystemTitle, self.settings.cipher.dedicatedKey, self.settings.cipher.authenticationKey)
|
|
507
|
-
else:
|
|
508
|
-
p = AesGcmParameter(self.settings.sourceSystemTitle, self.settings.cipher.blockCipherKey, self.settings.cipher.authenticationKey)
|
|
509
|
-
tmp = GXCiphering.decrypt(self.settings.cipher, p, reply.data)
|
|
510
|
-
reply.data.clear()
|
|
511
|
-
reply.data.set(tmp)
|
|
512
|
-
reply.command = XDLMSAPDU(reply.data.getUInt8())
|
|
513
|
-
if reply.command == XDLMSAPDU.DATA_NOTIFICATION or reply.command == XDLMSAPDU.INFORMATION_REPORT_REQUEST:
|
|
514
|
-
reply.command = None
|
|
515
|
-
reply.data.position = reply.data.position - 1
|
|
516
|
-
if isinstance(res_pdu := self.getPdu(reply), result.Error):
|
|
517
|
-
return res_pdu
|
|
518
|
-
else:
|
|
519
|
-
reply.data.position = reply.data.position - 1
|
|
520
|
-
case XDLMSAPDU.GLO_READ_RESPONSE | XDLMSAPDU.GLO_WRITE_RESPONSE | XDLMSAPDU.GLO_GET_RESPONSE | XDLMSAPDU.GLO_SET_RESPONSE | XDLMSAPDU.GLO_ACTION_RESPONSE | \
|
|
521
|
-
XDLMSAPDU.GENERAL_GLO_CIPHERING | XDLMSAPDU.GLO_EVENT_NOTIFICATION_REQUEST | XDLMSAPDU.DED_GET_RESPONSE | XDLMSAPDU.DED_SET_RESPONSE | \
|
|
522
|
-
XDLMSAPDU.DED_ACTION_RESPONSE | XDLMSAPDU.GENERAL_DED_CIPHERING | XDLMSAPDU.DED_EVENT_NOTIFICATION_REQUEST:
|
|
523
|
-
if self.settings.cipher is None:
|
|
524
|
-
return result.Error.from_e(ServiceError("Secure connection is not supported."), "get pdu")
|
|
525
|
-
if (reply.moreData & RequestTypes.FRAME) == 0:
|
|
526
|
-
reply.data.position = reply.data.position - 1
|
|
527
|
-
bb = GXByteBuffer(reply.data)
|
|
528
|
-
reply.data.size = reply.data.position = index
|
|
529
|
-
p = None
|
|
530
|
-
if self.settings.cipher.dedicatedKey and (OSI.APPLICATION in self.level):
|
|
531
|
-
p = AesGcmParameter(0, self.settings.sourceSystemTitle, self.settings.cipher.dedicatedKey, self.settings.cipher.authenticationKey)
|
|
532
|
-
else:
|
|
533
|
-
p = AesGcmParameter(0, self.settings.sourceSystemTitle, self.settings.cipher.blockCipherKey, self.settings.cipher.authenticationKey)
|
|
534
|
-
reply.data.set(GXCiphering.decrypt(self.settings.cipher, p, bb))
|
|
535
|
-
reply.command = None
|
|
536
|
-
if isinstance(res_pdu := self.getPdu(reply), result.Error):
|
|
537
|
-
return res_pdu
|
|
538
|
-
reply.cipherIndex = reply.data.size
|
|
539
|
-
case XDLMSAPDU.DATA_NOTIFICATION:
|
|
540
|
-
GXDLMS.handleDataNotification(self.settings, reply)
|
|
541
|
-
data = reply.data
|
|
542
|
-
start = data.position - 1
|
|
543
|
-
invokeId = data.getUInt32()
|
|
544
|
-
reply.time = None
|
|
545
|
-
len_ = data.getUInt8()
|
|
546
|
-
tmp = None
|
|
547
|
-
if len_ != 0:
|
|
548
|
-
tmp = bytearray(len_)
|
|
549
|
-
data.get(tmp)
|
|
550
|
-
dt = DataType.DATETIME
|
|
551
|
-
if len_ == 4:
|
|
552
|
-
dt = DataType.TIME
|
|
553
|
-
elif len_ == 5:
|
|
554
|
-
dt = DataType.DATE
|
|
555
|
-
info = _GXDataInfo()
|
|
556
|
-
info.type_ = dt
|
|
557
|
-
reply.time = _GXCommon.getData(self.settings, GXByteBuffer(tmp), info)
|
|
558
|
-
GXDLMS.getDataFromBlock(reply.data, start)
|
|
559
|
-
GXDLMS.getValueFromData(self.settings, reply)
|
|
560
|
-
case XDLMSAPDU.EVENT_NOTIFICATION_REQUEST:
|
|
561
|
-
pass
|
|
562
|
-
case XDLMSAPDU.INFORMATION_REPORT_REQUEST:
|
|
563
|
-
pass
|
|
564
|
-
case XDLMSAPDU.GENERAL_CIPHERING:
|
|
565
|
-
if self.settings.cipher is None:
|
|
566
|
-
return result.Error.from_e(ServiceError("Secure connection is not supported."), "get pdu")
|
|
567
|
-
if (reply.moreData & RequestTypes.FRAME) == 0:
|
|
568
|
-
reply.data.position = reply.data.position - 1
|
|
569
|
-
p = AesGcmParameter(0, self.settings.sourceSystemTitle, self.settings.cipher.blockCipherKey, self.settings.cipher.authenticationKey)
|
|
570
|
-
tmp = GXCiphering.decrypt(self.settings.cipher, p, reply.data)
|
|
571
|
-
reply.data.clear()
|
|
572
|
-
reply.data.set(tmp)
|
|
573
|
-
reply.command = None
|
|
574
|
-
if p.security:
|
|
575
|
-
if isinstance(res_pdu := self.getPdu(reply), result.Error):
|
|
576
|
-
return res_pdu
|
|
577
|
-
case XDLMSAPDU.GATEWAY_REQUEST:
|
|
578
|
-
pass
|
|
579
|
-
case XDLMSAPDU.GATEWAY_RESPONSE:
|
|
580
|
-
reply.data.getUInt8()
|
|
581
|
-
len_ = _GXCommon.getObjectCount(reply.data)
|
|
582
|
-
pda = bytearray(len_)
|
|
583
|
-
reply.data.get(pda)
|
|
584
|
-
GXDLMS.getDataFromBlock(reply, index)
|
|
585
|
-
reply.command = None
|
|
586
|
-
if isinstance(res_pdu := self.getPdu(reply), result.Error):
|
|
587
|
-
return res_pdu
|
|
588
|
-
case _:
|
|
589
|
-
return result.Error.from_e(ValueError("Invalid PDU Command."), "get pdu")
|
|
590
|
-
elif (reply.moreData & RequestTypes.FRAME) == 0:
|
|
591
|
-
if not reply.peek and reply.moreData == RequestTypes.NONE:
|
|
592
|
-
if reply.command == ACSEAPDU.AARE or reply.command == ACSEAPDU.AARQ:
|
|
593
|
-
reply.data.position = 0
|
|
594
|
-
else:
|
|
595
|
-
reply.data.position = 1
|
|
596
|
-
if reply.command == XDLMSAPDU.GENERAL_BLOCK_TRANSFER:
|
|
597
|
-
reply.data.position = reply.cipherIndex + 1
|
|
598
|
-
if isinstance(res_gbt := self.handleGbt(reply), result.Error):
|
|
599
|
-
return res_gbt
|
|
600
|
-
reply.cipherIndex = reply.data.size
|
|
601
|
-
reply.command = None
|
|
602
|
-
elif self.settings.isServer:
|
|
603
|
-
if reply.command in (
|
|
604
|
-
XDLMSAPDU.GLO_READ_REQUEST, XDLMSAPDU.GLO_WRITE_REQUEST, XDLMSAPDU.GLO_GET_REQUEST, XDLMSAPDU.GLO_SET_REQUEST, XDLMSAPDU.GLO_ACTION_REQUEST,
|
|
605
|
-
XDLMSAPDU.GLO_EVENT_NOTIFICATION_REQUEST, XDLMSAPDU.DED_GET_REQUEST, XDLMSAPDU.DED_SET_REQUEST, XDLMSAPDU.DED_ACTION_REQUEST,
|
|
606
|
-
XDLMSAPDU.DED_EVENT_NOTIFICATION_REQUEST):
|
|
607
|
-
reply.command = None
|
|
608
|
-
reply.data.position = reply.getCipherIndex()
|
|
609
|
-
if isinstance(res_pdu := self.getPdu(reply), result.Error):
|
|
610
|
-
return res_pdu
|
|
611
|
-
else:
|
|
612
|
-
reply.command = None
|
|
613
|
-
if reply.command in (
|
|
614
|
-
XDLMSAPDU.GLO_READ_RESPONSE,
|
|
615
|
-
XDLMSAPDU.GLO_WRITE_RESPONSE,
|
|
616
|
-
XDLMSAPDU.GLO_GET_RESPONSE,
|
|
617
|
-
XDLMSAPDU.GLO_SET_RESPONSE,
|
|
618
|
-
XDLMSAPDU.GLO_ACTION_RESPONSE,
|
|
619
|
-
XDLMSAPDU.DED_GET_RESPONSE,
|
|
620
|
-
XDLMSAPDU.DED_SET_RESPONSE,
|
|
621
|
-
XDLMSAPDU.DED_ACTION_RESPONSE,
|
|
622
|
-
XDLMSAPDU.GENERAL_GLO_CIPHERING,
|
|
623
|
-
XDLMSAPDU.GENERAL_DED_CIPHERING
|
|
624
|
-
):
|
|
625
|
-
reply.data.position = reply.cipherIndex
|
|
626
|
-
if isinstance(res_pdu := self.getPdu(reply), result.Error):
|
|
627
|
-
return res_pdu
|
|
628
|
-
if (
|
|
629
|
-
reply.command == XDLMSAPDU.READ_RESPONSE
|
|
630
|
-
and reply.totalCount > 1
|
|
631
|
-
):
|
|
632
|
-
if not GXDLMS.handleReadResponse(self.settings, reply, 0):
|
|
633
|
-
return result.OK
|
|
634
|
-
|
|
635
|
-
if (
|
|
636
|
-
reply.command == XDLMSAPDU.READ_RESPONSE
|
|
637
|
-
and reply.commandType == ReadResponse.DATA_BLOCK_RESULT
|
|
638
|
-
and (reply.moreData & RequestTypes.FRAME) != 0
|
|
639
|
-
):
|
|
640
|
-
return result.OK
|
|
641
|
-
if (
|
|
642
|
-
reply.data.position != reply.data.size
|
|
643
|
-
and (
|
|
644
|
-
reply.moreData == RequestTypes.NONE
|
|
645
|
-
or reply.peek)
|
|
646
|
-
and reply.command in (
|
|
647
|
-
XDLMSAPDU.READ_RESPONSE,
|
|
648
|
-
XDLMSAPDU.GET_RESPONSE,
|
|
649
|
-
XDLMSAPDU.ACTION_RESPONSE,
|
|
650
|
-
XDLMSAPDU.DATA_NOTIFICATION)
|
|
651
|
-
):
|
|
652
|
-
return result.OK
|
|
653
|
-
# GXDLMS.getValueFromData(self.settings, reply)
|
|
654
|
-
|
|
655
|
-
def __is_frame(self, notify, read_data: bytearray, reply_: GXReplyData) -> bool:
|
|
656
|
-
reply = GXByteBuffer(read_data)
|
|
657
|
-
is_notify: bool = False
|
|
658
|
-
match self.com_profile:
|
|
659
|
-
case c_pf.HDLC():
|
|
660
|
-
recv_frame = self.get_frame(read_data, reply_)
|
|
661
|
-
if recv_frame is not None:
|
|
662
|
-
self.log(logL.INFO, F"RX: {recv_frame.content.hex(' ')}")
|
|
663
|
-
if recv_frame.control == frame.Control.UI_PF:
|
|
664
|
-
target = notify # use instead of reply_ in getPdu(target). see in Gurux to do
|
|
665
|
-
is_notify = True
|
|
666
|
-
reply_.frameId = recv_frame.control
|
|
667
|
-
else: # TODO: GURUX redundant
|
|
668
|
-
# self.write_trace(F"RX {self.id}: {get_os_time()} {read_data}", TraceLevel.ERROR)
|
|
669
|
-
reply_.frameId = frame.Control(0)
|
|
670
|
-
case c_pf.TCPUDPIP(): # getTcpData TODO: check it
|
|
671
|
-
target = reply_
|
|
672
|
-
if len(reply) - reply.position < 8:
|
|
673
|
-
target.complete = False
|
|
674
|
-
return True
|
|
675
|
-
pos = reply.position
|
|
676
|
-
while reply.position < len(reply) - 1:
|
|
677
|
-
value = reply.getUInt16()
|
|
678
|
-
if value == 1:
|
|
679
|
-
# checkWrapperAddress
|
|
680
|
-
if self.settings.isServer:
|
|
681
|
-
value = reply.getUInt16()
|
|
682
|
-
if self.settings.clientAddress != 0 and self.settings.clientAddress != value:
|
|
683
|
-
raise Exception("Source addresses do not match. It is " + str(value) + ". It should be " + str(self.settings.clientAddress) + ".")
|
|
684
|
-
self.settings.clientAddress = value
|
|
685
|
-
value = reply.getUInt16()
|
|
686
|
-
if self.settings.serverAddress != 0 and self.settings.serverAddress != value:
|
|
687
|
-
raise Exception("Destination addresses do not match. It is " + str(value) + ". It should be " + str(self.settings.serverAddress) + ".")
|
|
688
|
-
self.settings.serverAddress = value
|
|
689
|
-
else:
|
|
690
|
-
value = reply.getUInt16()
|
|
691
|
-
if self.settings.clientAddress != 0 and self.settings.serverAddress != value:
|
|
692
|
-
if notify is None:
|
|
693
|
-
raise Exception("Source addresses do not match. It is " + str(value) + ". It should be " + str(self.settings.serverAddress) + ".")
|
|
694
|
-
notify.serverAddress = value
|
|
695
|
-
target = notify
|
|
696
|
-
else:
|
|
697
|
-
self.settings.serverAddress = value
|
|
698
|
-
value = reply.getUInt16()
|
|
699
|
-
if self.settings.clientAddress != 0 and self.settings.clientAddress != value:
|
|
700
|
-
if notify is None:
|
|
701
|
-
raise Exception("Destination addresses do not match. It is " + str(value) + ". It should be " + str(self.settings.clientAddress) + ".")
|
|
702
|
-
target = notify
|
|
703
|
-
notify.clientAddress = value
|
|
704
|
-
else:
|
|
705
|
-
self.settings.clientAddress = value
|
|
706
|
-
#
|
|
707
|
-
value = reply.getUInt16()
|
|
708
|
-
complete = not (len(reply) - reply.position) < value
|
|
709
|
-
if complete and (len(reply) - reply.position) != value:
|
|
710
|
-
self.log(logL.DEB, "Data length is " + str(value) + "and there are " + str(len(reply) - reply.position) + " bytes.")
|
|
711
|
-
target.complete = complete
|
|
712
|
-
if not complete:
|
|
713
|
-
reply.position = pos
|
|
714
|
-
else:
|
|
715
|
-
target.packetLength = (reply.position + value)
|
|
716
|
-
break
|
|
717
|
-
else:
|
|
718
|
-
reply.position = reply.position - 1
|
|
719
|
-
if target is not reply_:
|
|
720
|
-
is_notify = True
|
|
721
|
-
case c_pf.MBUS(): # not realised see how
|
|
722
|
-
GXDLMS.getMBusData(self.settings, reply, reply_)
|
|
723
|
-
case _: raise ValueError("Invalid Interface type.")
|
|
724
|
-
if not reply_.complete:
|
|
725
|
-
return False
|
|
726
|
-
|
|
727
|
-
# TODO: relocate notify to read_data_type
|
|
728
|
-
if notify and not is_notify:
|
|
729
|
-
#Check command to make sure it's not notify message.
|
|
730
|
-
if reply_.command in (XDLMSAPDU.DATA_NOTIFICATION,
|
|
731
|
-
XDLMSAPDU.GLO_EVENT_NOTIFICATION_REQUEST,
|
|
732
|
-
XDLMSAPDU.INFORMATION_REPORT_REQUEST,
|
|
733
|
-
XDLMSAPDU.EVENT_NOTIFICATION_REQUEST,
|
|
734
|
-
XDLMSAPDU.DED_INFORMATION_REPORT_REQUEST,
|
|
735
|
-
XDLMSAPDU.DED_EVENT_NOTIFICATION_REQUEST):
|
|
736
|
-
is_notify = True
|
|
737
|
-
notify.complete = reply_.complete
|
|
738
|
-
notify.command = reply_.command
|
|
739
|
-
reply_.command = None
|
|
740
|
-
reply_.time = None
|
|
741
|
-
notify.reply_.set(reply_.data)
|
|
742
|
-
# notify.value = reply_.value
|
|
743
|
-
reply_.data.trim()
|
|
744
|
-
if is_notify:
|
|
745
|
-
return False
|
|
746
|
-
return True
|
|
747
|
-
|
|
748
|
-
async def read_data_block(self) -> result.SimpleOrError[bytes]: # todo: make depend from CommunicationProfile
|
|
749
|
-
self.received_frames.clear()
|
|
750
|
-
reply = GXReplyData()
|
|
751
|
-
while self.send_frames:
|
|
752
|
-
send_frame = self.send_frames.popleft()
|
|
753
|
-
notify = GXReplyData()
|
|
754
|
-
reply.error = 0
|
|
755
|
-
recv_buf: bytearray = bytearray()
|
|
756
|
-
if not reply.isStreaming():
|
|
757
|
-
await self.media.send(send_frame.content)
|
|
758
|
-
self.log(logL.INFO, F"TX: {send_frame.content.hex(" ")}")
|
|
759
|
-
attempt: int = 1
|
|
760
|
-
while attempt < 3:
|
|
761
|
-
if not await self.media.receive(recv_buf): # todo: make for BLE
|
|
762
|
-
self.log(logL.WARN, F'Data receive failed: Try to resend {attempt + 1}/3. RX_buffer: {recv_buf.hex(" ")}')
|
|
763
|
-
await self.media.send(send_frame.content)
|
|
764
|
-
attempt += 1
|
|
765
|
-
continue
|
|
766
|
-
if self.__is_frame(notify, recv_buf, reply):
|
|
767
|
-
break
|
|
768
|
-
if notify.data.size != 0:
|
|
769
|
-
if not notify.isMoreData():
|
|
770
|
-
notify.clear()
|
|
771
|
-
continue
|
|
772
|
-
else:
|
|
773
|
-
"""our frame not was found"""
|
|
774
|
-
else:
|
|
775
|
-
return result.Error.from_e(TimeoutError("Failed to receive reply from the device in given time"), "read data block")
|
|
776
|
-
recv_buf.clear()
|
|
777
|
-
match reply.error:
|
|
778
|
-
case 0:
|
|
779
|
-
"""errors is absence"""
|
|
780
|
-
case 4:
|
|
781
|
-
return result.Error.from_e(exc.NoObject(), "read data block")
|
|
782
|
-
case _:
|
|
783
|
-
return result.Error.from_e(GXDLMSException(reply.error), "read data block")
|
|
784
|
-
if self.received_frames[-1].control.is_info() or self.received_frames[-1].control == frame.Control.UI_PF:
|
|
785
|
-
if self.received_frames[-1].is_segmentation:
|
|
786
|
-
"""pass handle frame. wait all information"""
|
|
787
|
-
else:
|
|
788
|
-
llc = sub_layer.LLC(frame.Frame.join_info(self.received_frames))
|
|
789
|
-
|
|
790
|
-
reply.data.position = len(reply.data)
|
|
791
|
-
reply.data.set(llc.info)
|
|
792
|
-
if isinstance(res_pdu := self.getPdu(reply), result.Error):
|
|
793
|
-
return res_pdu
|
|
794
|
-
# TODO: LLC to PDU
|
|
795
|
-
else:
|
|
796
|
-
received_frame = self.received_frames.popleft()
|
|
797
|
-
if send_frame.control == frame.Control.SNRM_P:
|
|
798
|
-
self.com_profile.negotiation.set_from_UA(received_frame.info)
|
|
799
|
-
self.log(logL.INFO, F"negotiation setup: {self.com_profile.negotiation}")
|
|
800
|
-
if reply.isMoreData():
|
|
801
|
-
if reply.isStreaming():
|
|
802
|
-
data = None
|
|
803
|
-
else:
|
|
804
|
-
# Generates an acknowledgment message, with which the server is informed to send next packets. Frame type. Acknowledgment message as byte array
|
|
805
|
-
if reply.moreData == RequestTypes.NONE:
|
|
806
|
-
return result.Error.from_e(ValueError("Invalid receiverReady RequestTypes parameter."), msg="read data block")
|
|
807
|
-
# Get next frame.
|
|
808
|
-
if (reply.moreData & RequestTypes.FRAME) != 0:
|
|
809
|
-
id_ = self.settings.getReceiverReady()
|
|
810
|
-
# return GXDLMS.getHdlcFrame(settings, id_, None)
|
|
811
|
-
self.add_frames_to_queue(frame.Control(id_))
|
|
812
|
-
else:
|
|
813
|
-
if self.settings.getUseLogicalNameReferencing():
|
|
814
|
-
if self.settings.isServer:
|
|
815
|
-
cmd = XDLMSAPDU.GET_RESPONSE
|
|
816
|
-
else:
|
|
817
|
-
cmd = XDLMSAPDU.GET_REQUEST
|
|
818
|
-
else:
|
|
819
|
-
if self.settings.isServer:
|
|
820
|
-
cmd = XDLMSAPDU.READ_RESPONSE
|
|
821
|
-
else:
|
|
822
|
-
cmd = XDLMSAPDU.READ_REQUEST
|
|
823
|
-
if reply.moreData == RequestTypes.GBT:
|
|
824
|
-
p = GXDLMSLNParameters(self.settings, 0, XDLMSAPDU.GENERAL_BLOCK_TRANSFER, 0, None, None, 0xff)
|
|
825
|
-
p.WindowSize = reply.windowSize
|
|
826
|
-
p.blockNumberAck = reply.blockNumberAck
|
|
827
|
-
p.blockIndex = reply.blockNumber
|
|
828
|
-
p.Streaming = False
|
|
829
|
-
messages = self.getLnMessages(p) # TODO: test it
|
|
830
|
-
else:
|
|
831
|
-
# Get next block.
|
|
832
|
-
bb = GXByteBuffer(4)
|
|
833
|
-
if self.settings.getUseLogicalNameReferencing():
|
|
834
|
-
bb.setUInt32(self.settings.blockIndex)
|
|
835
|
-
else:
|
|
836
|
-
bb.setUInt16(self.settings.blockIndex)
|
|
837
|
-
self.settings.increaseBlockIndex()
|
|
838
|
-
if self.settings.getUseLogicalNameReferencing():
|
|
839
|
-
p = GXDLMSLNParameters(self.settings, 0, cmd, pdu.GetResponse.WITH_DATABLOCK, bb, None, 0xff)
|
|
840
|
-
messages = self.getLnMessages(p)
|
|
841
|
-
else:
|
|
842
|
-
p = GXDLMSSNParameters(self.settings, cmd, 1, VariableAccessSpecification.BLOCK_NUMBER_ACCESS, bb, None)
|
|
843
|
-
messages = self.getSnMessages(p)
|
|
844
|
-
data = messages
|
|
845
|
-
return result.Simple(reply.data.get_data())
|
|
846
|
-
|
|
847
|
-
def getSnMessages(self, p: GXDLMSSNParameters):
|
|
848
|
-
reply = GXByteBuffer()
|
|
849
|
-
messages = list()
|
|
850
|
-
frame_ = 0x0
|
|
851
|
-
if p.command == XDLMSAPDU.INFORMATION_REPORT_REQUEST or p.command == XDLMSAPDU.DATA_NOTIFICATION:
|
|
852
|
-
frame_ = 0x13
|
|
853
|
-
while True:
|
|
854
|
-
ciphering = p.settings.cipher and p.settings.cipher.security != Security.NONE and p.command != ACSEAPDU.AARQ and p.command != ACSEAPDU.AARE
|
|
855
|
-
if (
|
|
856
|
-
not ciphering
|
|
857
|
-
and isinstance(self.com_profile, c_pf.HDLC)
|
|
858
|
-
):
|
|
859
|
-
if p.settings.isServer:
|
|
860
|
-
reply.set(_GXCommon.LLC_REPLY_BYTES)
|
|
861
|
-
elif not reply:
|
|
862
|
-
reply.set(_GXCommon.LLC_SEND_BYTES)
|
|
863
|
-
cnt = 0
|
|
864
|
-
cipherSize = 0
|
|
865
|
-
if ciphering:
|
|
866
|
-
cipherSize = GXDLMS._CIPHERING_HEADER_SIZE
|
|
867
|
-
if p.data:
|
|
868
|
-
cnt = p.data.size - p.data.position
|
|
869
|
-
if p.command == XDLMSAPDU.INFORMATION_REPORT_REQUEST:
|
|
870
|
-
reply.setUInt8(p.command)
|
|
871
|
-
if not p.time:
|
|
872
|
-
reply.setUInt8(cdt.NullData.TAG)
|
|
873
|
-
else:
|
|
874
|
-
pos = len(reply)
|
|
875
|
-
_GXCommon.setData(p.settings, reply, cdt.OctetString.TAG, p.time)
|
|
876
|
-
reply.move(pos + 1, pos, len(reply) - pos - 1)
|
|
877
|
-
_GXCommon.setObjectCount(p.count, reply)
|
|
878
|
-
reply.set(p.attributeDescriptor)
|
|
879
|
-
elif p.command != ACSEAPDU.AARQ and p.command != ACSEAPDU.AARE:
|
|
880
|
-
reply.setUInt8(p.command)
|
|
881
|
-
if p.count != 0xFF:
|
|
882
|
-
_GXCommon.setObjectCount(p.count, reply)
|
|
883
|
-
if p.requestType != 0xFF:
|
|
884
|
-
reply.setUInt8(p.requestType)
|
|
885
|
-
reply.set(p.attributeDescriptor)
|
|
886
|
-
if not p.settings.is_multiple_block():
|
|
887
|
-
p.multipleBlocks = len(reply) + cipherSize + cnt > p.settings.maxPduSize
|
|
888
|
-
if p.settings.is_multiple_block():
|
|
889
|
-
reply.size = 0
|
|
890
|
-
if (
|
|
891
|
-
not ciphering
|
|
892
|
-
and isinstance(self.com_profile, c_pf.HDLC)
|
|
893
|
-
):
|
|
894
|
-
if p.settings.isServer:
|
|
895
|
-
reply.set(_GXCommon.LLC_REPLY_BYTES)
|
|
896
|
-
elif not reply:
|
|
897
|
-
reply.set(_GXCommon.LLC_SEND_BYTES)
|
|
898
|
-
match p.command:
|
|
899
|
-
case XDLMSAPDU.WRITE_REQUEST:
|
|
900
|
-
p.requestType = VariableAccessSpecification.WRITE_DATA_BLOCK_ACCESS
|
|
901
|
-
case XDLMSAPDU.READ_REQUEST:
|
|
902
|
-
p.requestType = VariableAccessSpecification.READ_DATA_BLOCK_ACCESS
|
|
903
|
-
case XDLMSAPDU.READ_RESPONSE:
|
|
904
|
-
p.requestType = ReadResponse.DATA_BLOCK_RESULT
|
|
905
|
-
case _:
|
|
906
|
-
raise ValueError("Invalid command.")
|
|
907
|
-
reply.setUInt8(p.command)
|
|
908
|
-
reply.setUInt8(1)
|
|
909
|
-
if p.requestType != 0xFF:
|
|
910
|
-
reply.setUInt8(p.requestType)
|
|
911
|
-
cnt = GXDLMS.appendMultipleSNBlocks(p, reply)
|
|
912
|
-
else:
|
|
913
|
-
cnt = GXDLMS.appendMultipleSNBlocks(p, reply)
|
|
914
|
-
if p.data:
|
|
915
|
-
reply.set(p.data, p.data.position, cnt)
|
|
916
|
-
if p.data and p.data.position == p.data.size:
|
|
917
|
-
p.settings.index = 0
|
|
918
|
-
p.settings.count = 0
|
|
919
|
-
if ciphering and p.command != ACSEAPDU.AARQ and p.command != ACSEAPDU.AARE:
|
|
920
|
-
cipher = p.settings.cipher
|
|
921
|
-
s = AesGcmParameter(self.getGloMessage(p.command), cipher.systemTitle, cipher.blockCipherKey, cipher.authenticationKey)
|
|
922
|
-
s.security = cipher.security
|
|
923
|
-
s.invocationCounter = cipher.invocationCounter
|
|
924
|
-
tmp = GXCiphering.encrypt(s, reply.array())
|
|
925
|
-
assert not tmp
|
|
926
|
-
reply.size = 0
|
|
927
|
-
if isinstance(self.com_profile, c_pf.HDLC):
|
|
928
|
-
if p.settings.isServer:
|
|
929
|
-
reply.set(_GXCommon.LLC_REPLY_BYTES)
|
|
930
|
-
elif not reply:
|
|
931
|
-
reply.set(_GXCommon.LLC_SEND_BYTES)
|
|
932
|
-
reply.set(tmp)
|
|
933
|
-
if p.command != ACSEAPDU.AARQ and p.command != ACSEAPDU.AARE:
|
|
934
|
-
assert not p.settings.maxPduSize < len(reply)
|
|
935
|
-
while reply.position != len(reply):
|
|
936
|
-
match self.com_profile:
|
|
937
|
-
case c_pf.TCPUDPIP():
|
|
938
|
-
messages.append(GXDLMS.getWrapperFrame(p.settings, p.command, reply))
|
|
939
|
-
case c_pf.HDLC():
|
|
940
|
-
messages.append(GXDLMS.getHdlcFrame(p.settings, frame_, reply))
|
|
941
|
-
if reply.position != len(reply):
|
|
942
|
-
frame_ = p.settings.getNextSend(False)
|
|
943
|
-
case _:
|
|
944
|
-
raise ValueError("InterfaceType")
|
|
945
|
-
reply.clear()
|
|
946
|
-
frame_ = 0
|
|
947
|
-
if not p.data or p.data.position == p.data.size:
|
|
948
|
-
break
|
|
949
|
-
return messages
|
|
950
|
-
|
|
951
|
-
def receiverReady(self, reply):
|
|
952
|
-
""" Generates an acknowledgment message, with which the server is informed to send next packets. Frame type. Acknowledgment message as byte array. """
|
|
953
|
-
if reply.moreData == RequestTypes.NONE:
|
|
954
|
-
raise ValueError("Invalid receiverReady RequestTypes parameter.")
|
|
955
|
-
# Get next frame.
|
|
956
|
-
if (reply.moreData & RequestTypes.FRAME) != 0:
|
|
957
|
-
id_ = self.settings.getReceiverReady()
|
|
958
|
-
# return GXDLMS.getHdlcFrame(settings, id_, None)
|
|
959
|
-
return self.add_frames_to_queue(frame.Control(id_))
|
|
960
|
-
if self.settings.getUseLogicalNameReferencing():
|
|
961
|
-
if self.settings.isServer:
|
|
962
|
-
cmd = XDLMSAPDU.GET_RESPONSE
|
|
963
|
-
else:
|
|
964
|
-
cmd = XDLMSAPDU.GET_REQUEST
|
|
965
|
-
else:
|
|
966
|
-
if self.settings.isServer:
|
|
967
|
-
cmd = XDLMSAPDU.READ_RESPONSE
|
|
968
|
-
else:
|
|
969
|
-
cmd = XDLMSAPDU.READ_REQUEST
|
|
970
|
-
|
|
971
|
-
if reply.moreData == RequestTypes.GBT:
|
|
972
|
-
p = GXDLMSLNParameters(self.settings, 0, XDLMSAPDU.GENERAL_BLOCK_TRANSFER, 0, None, None, 0xff)
|
|
973
|
-
p.WindowSize = reply.windowSize
|
|
974
|
-
p.blockNumberAck = reply.blockNumberAck
|
|
975
|
-
p.blockIndex = reply.blockNumber
|
|
976
|
-
p.Streaming = False
|
|
977
|
-
reply = self.getLnMessages(p) # TODO: test it
|
|
978
|
-
else:
|
|
979
|
-
# Get next block.
|
|
980
|
-
bb = GXByteBuffer(4)
|
|
981
|
-
if self.settings.getUseLogicalNameReferencing():
|
|
982
|
-
bb.setUInt32(self.settings.blockIndex)
|
|
983
|
-
else:
|
|
984
|
-
bb.setUInt16(self.settings.blockIndex)
|
|
985
|
-
self.settings.increaseBlockIndex()
|
|
986
|
-
if self.settings.getUseLogicalNameReferencing():
|
|
987
|
-
p = GXDLMSLNParameters(self.settings, 0, cmd, pdu.GetResponse.WITH_DATABLOCK, bb, None, 0xff)
|
|
988
|
-
reply = self.getLnMessages(p)
|
|
989
|
-
else:
|
|
990
|
-
p = GXDLMSSNParameters(self.settings, cmd, 1, VariableAccessSpecification.BLOCK_NUMBER_ACCESS, bb, None)
|
|
991
|
-
reply = self.getSnMessages(p)
|
|
992
|
-
return reply
|
|
993
|
-
|
|
994
|
-
def set_params(self, field: str, value: str):
|
|
995
|
-
self.__dict__[field] = eval(value)
|
|
996
|
-
|
|
997
|
-
async def close(self) -> result.StrictOk | result.Error:
|
|
998
|
-
"""close , media is open"""
|
|
999
|
-
res = result.StrictOk()
|
|
1000
|
-
self.log(logL.DEB, "close")
|
|
1001
|
-
if self.level > OSI.DATA_LINK:
|
|
1002
|
-
# Release is call only for secured connections. All meters are not supporting Release and it's causing problems.
|
|
1003
|
-
if (
|
|
1004
|
-
isinstance(self.com_profile, c_pf.TCPUDPIP)
|
|
1005
|
-
or (
|
|
1006
|
-
isinstance(self.com_profile, c_pf.HDLC)
|
|
1007
|
-
and self.settings.cipher.security != Security.NONE
|
|
1008
|
-
)
|
|
1009
|
-
):
|
|
1010
|
-
self.releaseRequest()
|
|
1011
|
-
if isinstance(res_rdb := await self.read_data_block(), result.Error):
|
|
1012
|
-
res.append_err(res_rdb.err)
|
|
1013
|
-
self.log(logL.WARN, "don't support release ReleaseRequest")
|
|
1014
|
-
self.level = OSI.DATA_LINK
|
|
1015
|
-
# hdlc close
|
|
1016
|
-
if isinstance(res_diconnect_req := await self.disconnect_request(), result.Error):
|
|
1017
|
-
res.append_err(res_diconnect_req.err)
|
|
1018
|
-
self.level -= OSI.DATA_LINK
|
|
1019
|
-
return res
|
|
1020
|
-
|
|
1021
|
-
async def disconnect_request(self) -> result.Ok | result.Error:
|
|
1022
|
-
""" Sent to server DISC """
|
|
1023
|
-
if isinstance(self.com_profile, c_pf.HDLC):
|
|
1024
|
-
self.add_frames_to_queue(frame.Control.DISC_P)
|
|
1025
|
-
else:
|
|
1026
|
-
self.releaseRequest()
|
|
1027
|
-
return await self.read_data_block()
|
|
1028
|
-
|
|
1029
|
-
@cached_property
|
|
1030
|
-
def n_phases(self) -> int:
|
|
1031
|
-
"""cached phases amount"""
|
|
1032
|
-
return self.objects.get_n_phases()
|
|
1033
|
-
|
|
1034
|
-
async def encode(self,
|
|
1035
|
-
obj: ic.COSEMInterfaceClasses,
|
|
1036
|
-
index: int,
|
|
1037
|
-
value: str | int) -> cdt.CommonDataType:
|
|
1038
|
-
"""encode attribute value from string if possible, else return None(for CHOICE variant) during connection"""
|
|
1039
|
-
if (ret := obj.encode(index, value)) is not None:
|
|
1040
|
-
return ret
|
|
1041
|
-
else:
|
|
1042
|
-
await self.read_attribute(obj, index)
|
|
1043
|
-
ret = obj.get_attr(index).copy()
|
|
1044
|
-
ret.set(value)
|
|
1045
|
-
return ret
|
|
1046
|
-
|
|
1047
|
-
# TODO: remove in future
|
|
1048
|
-
def parseApplicationAssociationResponse(self, data: bytes):
|
|
1049
|
-
""" Parse server's challenge if HLS authentication is used. Received reply from the server. todo: refactoring here """
|
|
1050
|
-
ic = 0
|
|
1051
|
-
value = cdt.OctetString(data)
|
|
1052
|
-
match self.m_id:
|
|
1053
|
-
case mechanism_id.HIGH_GMAC:
|
|
1054
|
-
secret = self.settings.sourceSystemTitle
|
|
1055
|
-
bb = GXByteBuffer(value)
|
|
1056
|
-
bb.getUInt8()
|
|
1057
|
-
ic = bb.getUInt32()
|
|
1058
|
-
case mechanism_id.HIGH_SHA256:
|
|
1059
|
-
tmp2 = GXByteBuffer()
|
|
1060
|
-
tmp2.set(self.secret)
|
|
1061
|
-
tmp2.set(self.settings.sourceSystemTitle)
|
|
1062
|
-
tmp2.set(self.settings.cipher.systemTitle)
|
|
1063
|
-
tmp2.set(self.settings.ctoSChallenge)
|
|
1064
|
-
tmp2.set(self.settings.stoCChallenge)
|
|
1065
|
-
secret = tmp2.array()
|
|
1066
|
-
case mechanism_id.HIGH: secret = self.secret
|
|
1067
|
-
case mechanism_id.HIGH_ECDSA: raise ValueError("ECDSA is not supported.")
|
|
1068
|
-
case _ as mech_id: raise ValueError(F'{mech_id} is not supported')
|
|
1069
|
-
tmp = self.secure(ic, self.settings.ctoSChallenge, bytes(secret))
|
|
1070
|
-
challenge = cdt.OctetString(bytearray(tmp))
|
|
1071
|
-
equals = challenge == value
|
|
1072
|
-
if not equals:
|
|
1073
|
-
self.log(logL.DEB, "Invalid StoC:" + GXByteBuffer.hex(value, True) + "-" + GXByteBuffer.hex(tmp, True))
|
|
1074
|
-
if not equals:
|
|
1075
|
-
raise Exception("parseApplicationAssociationResponse failed. " + " Server to Client do not match.")
|
|
1076
|
-
self.level |= OSI.APPLICATION
|
|
1077
|
-
|
|
1078
|
-
def secure(self, ic, data, secret: bytes) -> bytes:
|
|
1079
|
-
""" TODO: """
|
|
1080
|
-
if not isinstance(secret, bytes):
|
|
1081
|
-
raise ValueError(F'cipher is not bytes type, got {secret.__class__}')
|
|
1082
|
-
# Get server Challenge.
|
|
1083
|
-
challenge = GXByteBuffer()
|
|
1084
|
-
# Get shared secret
|
|
1085
|
-
match self.m_id:
|
|
1086
|
-
case mechanism_id.HIGH:
|
|
1087
|
-
if len(secret) != 16:
|
|
1088
|
-
raise ValueError(F'length secret must be 16, got {len(secret)}')
|
|
1089
|
-
cipher = AES.new(secret, AES.MODE_ECB)
|
|
1090
|
-
ciphertext: bytes = cipher.encrypt(copy_with_align(data))
|
|
1091
|
-
return ciphertext
|
|
1092
|
-
case mechanism_id.HIGH_GMAC:
|
|
1093
|
-
challenge.set(data)
|
|
1094
|
-
d = challenge.array()
|
|
1095
|
-
# SC is always Security.Authentication.
|
|
1096
|
-
p = AesGcmParameter(0, secret, self.settings.cipher.blockCipherKey, self.settings.cipher.authenticationKey)
|
|
1097
|
-
p.security = Security.AUTHENTICATION
|
|
1098
|
-
p.invocationCounter = ic
|
|
1099
|
-
p.type_ = CountType.TAG
|
|
1100
|
-
challenge.clear()
|
|
1101
|
-
challenge.setUInt8(Security.AUTHENTICATION)
|
|
1102
|
-
challenge.setUInt32(p.invocationCounter)
|
|
1103
|
-
challenge.set(GXDLMSChippering.encryptAesGcm(p, d))
|
|
1104
|
-
return challenge.array()
|
|
1105
|
-
case mechanism_id.HIGH_SHA256:
|
|
1106
|
-
challenge.set(secret)
|
|
1107
|
-
d = challenge.array()
|
|
1108
|
-
md = hashlib.sha256()
|
|
1109
|
-
md.update(d)
|
|
1110
|
-
return md.digest()
|
|
1111
|
-
case mechanism_id.HIGH_MD5:
|
|
1112
|
-
challenge.set(data)
|
|
1113
|
-
challenge.set(secret)
|
|
1114
|
-
d = challenge.array()
|
|
1115
|
-
md = hashlib.md5()
|
|
1116
|
-
md.update(d)
|
|
1117
|
-
return md.digest()
|
|
1118
|
-
case mechanism_id.HIGH_SHA1:
|
|
1119
|
-
challenge.set(data)
|
|
1120
|
-
challenge.set(secret)
|
|
1121
|
-
d = challenge.array()
|
|
1122
|
-
md = hashlib.sha1()
|
|
1123
|
-
md.update(d)
|
|
1124
|
-
return md.digest()
|
|
1125
|
-
case mechanism_id.HIGH_ECDSA: raise Exception("ECDSA is not supported.")
|
|
1126
|
-
case _ as err: raise Exception(F'Not support {err}')
|
|
1127
|
-
|
|
1128
|
-
def getApplicationAssociationRequest(self):
|
|
1129
|
-
""" Get challenge request if HLS authentication is used. """
|
|
1130
|
-
match self.m_id, self.secret:
|
|
1131
|
-
case mechanism_id.HIGH_ECDSA | mechanism_id.HIGH_GMAC, None: raise ValueError('Password is invalid.')
|
|
1132
|
-
case _: pass
|
|
1133
|
-
self.settings.resetBlockIndex()
|
|
1134
|
-
match self.m_id:
|
|
1135
|
-
case mechanism_id.HIGH_GMAC: pw = self.settings.cipher.systemTitle
|
|
1136
|
-
case mechanism_id.HIGH_SHA256:
|
|
1137
|
-
tmp = GXByteBuffer()
|
|
1138
|
-
tmp.set(self.secret)
|
|
1139
|
-
tmp.set(self.settings.cipher.systemTitle)
|
|
1140
|
-
tmp.set(self.settings.sourceSystemTitle)
|
|
1141
|
-
tmp.set(self.settings.stoCChallenge)
|
|
1142
|
-
tmp.set(self.settings.ctoSChallenge)
|
|
1143
|
-
pw = tmp.array()
|
|
1144
|
-
case _: pw = self.secret
|
|
1145
|
-
ic = 0
|
|
1146
|
-
if self.settings.cipher:
|
|
1147
|
-
ic = self.settings.cipher.invocationCounter
|
|
1148
|
-
challenge = self.secure(ic, self.settings.getStoCChallenge(), pw)
|
|
1149
|
-
if self.settings.getUseLogicalNameReferencing():
|
|
1150
|
-
return self.get_action_request_normal(
|
|
1151
|
-
meth_desc=ut.CosemMethodDescriptor((overview.ClassID.ASSOCIATION_LN, ut.CosemObjectInstanceId(F"0.0.40.0.0.255"), ut.CosemObjectMethodId(1))),
|
|
1152
|
-
# meth_desc=self.current_association.get_meth_descriptor(1),
|
|
1153
|
-
method=method.ReplyToHLSAuthentication(bytearray(challenge)))
|
|
1154
|
-
else:
|
|
1155
|
-
return self.method2(0xFA00, 12, 8, challenge, cdt.OctetString.TAG) # TODO: rewrite old client.method
|
|
1156
|
-
|
|
1157
|
-
def parseAARE(self, pdu: bytes) -> AcseServiceUser:
|
|
1158
|
-
# Get AARE tag and length
|
|
1159
|
-
buff = GXByteBuffer(pdu)
|
|
1160
|
-
tag = buff.getUInt8()
|
|
1161
|
-
if self.settings.isServer:
|
|
1162
|
-
if tag != (BerType.APPLICATION | BerType.CONSTRUCTED | AARQapdu.PROTOCOL_VERSION):
|
|
1163
|
-
raise ValueError("Invalid tag.")
|
|
1164
|
-
else:
|
|
1165
|
-
if tag != (BerType.APPLICATION | BerType.CONSTRUCTED | AARQapdu.APPLICATION_CONTEXT_NAME):
|
|
1166
|
-
raise ValueError("Invalid tag.")
|
|
1167
|
-
if _GXCommon.getObjectCount(buff) > len(buff) - buff.position:
|
|
1168
|
-
raise ValueError("PDU: Not enough data.")
|
|
1169
|
-
resultComponent = AssociationResult.ACCEPTED
|
|
1170
|
-
resultDiagnosticValue = AcseServiceUser.NULL
|
|
1171
|
-
len_ = 0
|
|
1172
|
-
tag = 0
|
|
1173
|
-
while buff.position < len(buff):
|
|
1174
|
-
tag = buff.getUInt8()
|
|
1175
|
-
if tag == BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.APPLICATION_CONTEXT_NAME: # 0xA1
|
|
1176
|
-
# Get length.
|
|
1177
|
-
len_ = buff.getUInt8()
|
|
1178
|
-
if len(buff) - buff.position < len_:
|
|
1179
|
-
raise ValueError("Encoding failed. Not enough data.")
|
|
1180
|
-
if buff.getUInt8() != 0x6:
|
|
1181
|
-
raise ValueError("Encoding failed. Not an Object ID.")
|
|
1182
|
-
if self.settings.isServer and self.settings.cipher:
|
|
1183
|
-
self.settings.cipher.setSecurity(Security.NONE)
|
|
1184
|
-
# Object ID length.
|
|
1185
|
-
len_ = buff.getUInt8()
|
|
1186
|
-
tmp = bytearray(len_)
|
|
1187
|
-
buff.get(tmp)
|
|
1188
|
-
if tmp[:6] != bytearray(b'\x60\x85\x74\x05\x08\x01'):
|
|
1189
|
-
raise Exception("Encoding failed. Invalid Application context name.")
|
|
1190
|
-
match tmp[6], self.settings.getUseLogicalNameReferencing():
|
|
1191
|
-
case 1 | 3, True: pass
|
|
1192
|
-
case 2 | 4, False: pass
|
|
1193
|
-
case _: raise GXDLMSException(AssociationResult.REJECTED_PERMANENT, AcseServiceUser.APPLICATION_CONTEXT_NAME_NOT_SUPPORTED)
|
|
1194
|
-
elif tag == BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.CALLED_AP_TITLE: # 0xA2
|
|
1195
|
-
# Get length.
|
|
1196
|
-
if buff.getUInt8() != 3:
|
|
1197
|
-
raise ValueError("Invalid tag.")
|
|
1198
|
-
if self.settings.isServer:
|
|
1199
|
-
# Choice for result (INTEGER, universal)
|
|
1200
|
-
if buff.getUInt8() != BerType.OCTET_STRING:
|
|
1201
|
-
raise ValueError("Invalid tag.")
|
|
1202
|
-
len_ = buff.getUInt8()
|
|
1203
|
-
tmp = bytearray(len_)
|
|
1204
|
-
buff.get(tmp)
|
|
1205
|
-
try:
|
|
1206
|
-
self.settings.sourceSystemTitle = tmp
|
|
1207
|
-
except Exception as ex:
|
|
1208
|
-
raise ex
|
|
1209
|
-
else:
|
|
1210
|
-
# Choice for result (INTEGER, universal)
|
|
1211
|
-
if buff.getUInt8() != BerType.INTEGER:
|
|
1212
|
-
raise ValueError("Invalid tag.")
|
|
1213
|
-
# Get length.
|
|
1214
|
-
if buff.getUInt8() != 1:
|
|
1215
|
-
raise ValueError("Invalid tag.")
|
|
1216
|
-
resultComponent = AssociationResult(buff.getUInt8())
|
|
1217
|
-
elif tag == BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.CALLED_AE_QUALIFIER: # 0xA3
|
|
1218
|
-
tag = int()
|
|
1219
|
-
resultDiagnosticValue = AcseServiceUser.NULL
|
|
1220
|
-
len_ = buff.getUInt8()
|
|
1221
|
-
# ACSE service user tag.
|
|
1222
|
-
tag = buff.getUInt8()
|
|
1223
|
-
len_ = buff.getUInt8()
|
|
1224
|
-
if self.settings.isServer:
|
|
1225
|
-
calledAEQualifier = bytearray(len_)
|
|
1226
|
-
buff.get(calledAEQualifier)
|
|
1227
|
-
else:
|
|
1228
|
-
# Result source diagnostic component.
|
|
1229
|
-
tag = buff.getUInt8()
|
|
1230
|
-
if tag != BerType.INTEGER:
|
|
1231
|
-
raise ValueError("Invalid tag.")
|
|
1232
|
-
len_ = buff.getUInt8()
|
|
1233
|
-
if len_ != 1:
|
|
1234
|
-
raise ValueError("Invalid tag.")
|
|
1235
|
-
resultDiagnosticValue = AcseServiceUser(buff.getUInt8())
|
|
1236
|
-
elif tag == BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.CALLED_AP_INVOCATION_ID: # 0xA4
|
|
1237
|
-
if self.settings.isServer:
|
|
1238
|
-
# Get len.
|
|
1239
|
-
if buff.getUInt8() != 3:
|
|
1240
|
-
raise ValueError("Invalid tag.")
|
|
1241
|
-
# Choice for result (Universal, Octetstring type)
|
|
1242
|
-
if buff.getUInt8() != BerType.INTEGER:
|
|
1243
|
-
raise ValueError("Invalid tag.")
|
|
1244
|
-
if buff.getUInt8() != 1:
|
|
1245
|
-
raise ValueError("Invalid tag length.")
|
|
1246
|
-
# Get value.
|
|
1247
|
-
len_ = buff.getUInt8()
|
|
1248
|
-
else:
|
|
1249
|
-
# Get length.
|
|
1250
|
-
if buff.getUInt8() != 0xA:
|
|
1251
|
-
raise ValueError("Invalid tag.")
|
|
1252
|
-
# Choice for result (Universal, Octet string type)
|
|
1253
|
-
if buff.getUInt8() != BerType.OCTET_STRING:
|
|
1254
|
-
raise ValueError("Invalid tag.")
|
|
1255
|
-
# responding-AP-title-field
|
|
1256
|
-
# Get length.
|
|
1257
|
-
len_ = buff.getUInt8()
|
|
1258
|
-
tmp = bytearray(len_)
|
|
1259
|
-
buff.get(tmp)
|
|
1260
|
-
self.settings.setSourceSystemTitle(tmp)
|
|
1261
|
-
elif tag == BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.CALLED_AE_INVOCATION_ID: # 0xA5
|
|
1262
|
-
len_ = buff.getUInt8()
|
|
1263
|
-
tag = buff.getUInt8()
|
|
1264
|
-
len_ = buff.getUInt8()
|
|
1265
|
-
self.settings.userId = buff.getUInt8()
|
|
1266
|
-
elif tag == BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.CALLING_AP_TITLE: # 0xA6
|
|
1267
|
-
len_ = buff.getUInt8()
|
|
1268
|
-
tag = buff.getUInt8()
|
|
1269
|
-
len_ = buff.getUInt8()
|
|
1270
|
-
tmp = bytearray(len_)
|
|
1271
|
-
buff.get(tmp)
|
|
1272
|
-
try:
|
|
1273
|
-
self.settings.setSourceSystemTitle(tmp)
|
|
1274
|
-
except Exception as ex:
|
|
1275
|
-
raise ex
|
|
1276
|
-
elif tag == BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.SENDER_ACSE_REQUIREMENTS: # 0xAA
|
|
1277
|
-
len_ = buff.getUInt8()
|
|
1278
|
-
tag = buff.getUInt8()
|
|
1279
|
-
len_ = buff.getUInt8()
|
|
1280
|
-
tmp = bytearray(len_)
|
|
1281
|
-
buff.get(tmp)
|
|
1282
|
-
self.settings.setStoCChallenge(tmp)
|
|
1283
|
-
elif tag == BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.CALLING_AE_QUALIFIER: # 0xA7
|
|
1284
|
-
len_ = buff.getUInt8()
|
|
1285
|
-
tag = buff.getUInt8()
|
|
1286
|
-
len_ = buff.getUInt8()
|
|
1287
|
-
self.settings.userId = buff.getUInt8()
|
|
1288
|
-
elif tag == BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.CALLING_AP_INVOCATION_ID: # 0xA8
|
|
1289
|
-
if buff.getUInt8() != 3:
|
|
1290
|
-
raise ValueError("Invalid tag.")
|
|
1291
|
-
if buff.getUInt8() != 2:
|
|
1292
|
-
raise ValueError("Invalid length.")
|
|
1293
|
-
if buff.getUInt8() != 1:
|
|
1294
|
-
raise ValueError("Invalid tag length.")
|
|
1295
|
-
# Get value.
|
|
1296
|
-
len_ = buff.getUInt8()
|
|
1297
|
-
elif tag == BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.CALLING_AE_INVOCATION_ID: # 0xA9
|
|
1298
|
-
len_ = buff.getUInt8()
|
|
1299
|
-
tag = buff.getUInt8()
|
|
1300
|
-
len_ = buff.getUInt8()
|
|
1301
|
-
self.settings.userId = buff.getUInt8()
|
|
1302
|
-
elif tag in (BerType.CONTEXT | AARQapdu.SENDER_ACSE_REQUIREMENTS, BerType.CONTEXT | AARQapdu.CALLING_AP_INVOCATION_ID): # 0x88
|
|
1303
|
-
# Get sender ACSE-requirements field component.
|
|
1304
|
-
if buff.getUInt8() != 2:
|
|
1305
|
-
raise ValueError("Invalid tag.")
|
|
1306
|
-
if buff.getUInt8() != BerType.OBJECT_DESCRIPTOR:
|
|
1307
|
-
raise ValueError("Invalid tag.")
|
|
1308
|
-
# Get only value because client application is
|
|
1309
|
-
# sending system title with LOW authentication.
|
|
1310
|
-
buff.getUInt8()
|
|
1311
|
-
elif tag in (BerType.CONTEXT | AARQapdu.MECHANISM_NAME, BerType.CONTEXT | AARQapdu.CALLING_AE_INVOCATION_ID): # 0x89
|
|
1312
|
-
ch = buff.getUInt8()
|
|
1313
|
-
if buff.getUInt8() != 0x60:
|
|
1314
|
-
raise ValueError("Invalid tag.")
|
|
1315
|
-
if buff.getUInt8() != 0x85:
|
|
1316
|
-
raise ValueError("Invalid tag.")
|
|
1317
|
-
if buff.getUInt8() != 0x74:
|
|
1318
|
-
raise ValueError("Invalid tag.")
|
|
1319
|
-
if buff.getUInt8() != 0x05:
|
|
1320
|
-
raise ValueError("Invalid tag.")
|
|
1321
|
-
if buff.getUInt8() != 0x08:
|
|
1322
|
-
raise ValueError("Invalid tag.")
|
|
1323
|
-
if buff.getUInt8() != 0x02:
|
|
1324
|
-
raise ValueError("Invalid tag.")
|
|
1325
|
-
ch = buff.getUInt8()
|
|
1326
|
-
self.m_id.set(ch) # TODO: maybe check with current?
|
|
1327
|
-
elif tag == BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.CALLING_AUTHENTICATION_VALUE: # 0xAC
|
|
1328
|
-
len_ = buff.getUInt8()
|
|
1329
|
-
# Get authentication information.
|
|
1330
|
-
if buff.getUInt8() != 0x80:
|
|
1331
|
-
raise ValueError("Invalid tag.")
|
|
1332
|
-
len_ = buff.getUInt8()
|
|
1333
|
-
tmp = bytearray(len_)
|
|
1334
|
-
buff.get(tmp)
|
|
1335
|
-
match self.m_id:
|
|
1336
|
-
case mechanism_id.LOW: self.settings.password = tmp
|
|
1337
|
-
case _: self.settings.ctoSChallenge = tmp
|
|
1338
|
-
elif tag == BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.USER_INFORMATION: # 0xBE
|
|
1339
|
-
# Check result component. Some meters are returning invalid user-information if connection failed.
|
|
1340
|
-
# if resultComponent != AssociationResult.ACCEPTED and resultDiagnosticValue != SourceDiagnostic.NONE:
|
|
1341
|
-
# raise exc.AssociationResultError(resultComponent)
|
|
1342
|
-
try:
|
|
1343
|
-
len_ = buff.getUInt8()
|
|
1344
|
-
if len(buff) - buff.position < len_:
|
|
1345
|
-
raise ValueError("Not enough data.")
|
|
1346
|
-
# Encoding the choice for user information
|
|
1347
|
-
tag = buff.getUInt8()
|
|
1348
|
-
if tag != 0x4:
|
|
1349
|
-
raise ValueError("Invalid tag.")
|
|
1350
|
-
len_ = buff.getUInt8()
|
|
1351
|
-
if len(buff) - buff.position < len_:
|
|
1352
|
-
raise ValueError("Not enough data.")
|
|
1353
|
-
# Tag for xDLMS-Initate.response
|
|
1354
|
-
tag = buff.getUInt8()
|
|
1355
|
-
originalPos = 0
|
|
1356
|
-
if tag in (XDLMSAPDU.GLO_INITIATE_RESPONSE, XDLMSAPDU.GLO_INITIATE_REQUEST,
|
|
1357
|
-
XDLMSAPDU.GENERAL_GLO_CIPHERING, XDLMSAPDU.GENERAL_DED_CIPHERING):
|
|
1358
|
-
buff.position = buff.position - 1
|
|
1359
|
-
p = AesGcmParameter(0, self.settings.sourceSystemTitle, self.settings.cipher.blockCipherKey, self.settings.cipher.authenticationKey)
|
|
1360
|
-
tmp = GXCiphering.decrypt(self.settings.cipher, p, buff)
|
|
1361
|
-
buff.size = 0
|
|
1362
|
-
buff.set(tmp)
|
|
1363
|
-
self.settings.cipher.security = p.security
|
|
1364
|
-
self.settings.cipher.securitySuite = p.securitySuite
|
|
1365
|
-
tag = buff.getUInt8()
|
|
1366
|
-
tmp2 = GXByteBuffer()
|
|
1367
|
-
tmp2.setUInt8(0)
|
|
1368
|
-
tag2 = XDLMSAPDU(tag) # TODO: remove it
|
|
1369
|
-
response = tag2 == XDLMSAPDU.INITIATE_RESPONSE
|
|
1370
|
-
if response:
|
|
1371
|
-
# Optional usage field of the negotiated quality of service component
|
|
1372
|
-
tag = buff.getUInt8()
|
|
1373
|
-
if tag != 0:
|
|
1374
|
-
len_ = buff.getUInt8()
|
|
1375
|
-
buff.position = buff.position + len_
|
|
1376
|
-
elif tag2 == XDLMSAPDU.INITIATE_REQUEST:
|
|
1377
|
-
# Optional usage field of the negotiated quality of service component
|
|
1378
|
-
tag = buff.getUInt8()
|
|
1379
|
-
if tag != 0:
|
|
1380
|
-
len_ = buff.getUInt8()
|
|
1381
|
-
tmp = bytearray(len_)
|
|
1382
|
-
buff.get(tmp)
|
|
1383
|
-
if self.settings.cipher:
|
|
1384
|
-
self.settings.cipher.setDedicatedKey(tmp)
|
|
1385
|
-
elif self.settings.cipher:
|
|
1386
|
-
self.settings.cipher.dedicatedKey = None
|
|
1387
|
-
# Optional usage field of the negotiated quality of service component
|
|
1388
|
-
tag = buff.getUInt8()
|
|
1389
|
-
if tag != 0:
|
|
1390
|
-
len_ = buff.getUInt8()
|
|
1391
|
-
# Optional usage field of the proposed quality of service component
|
|
1392
|
-
tag = buff.getUInt8()
|
|
1393
|
-
# Skip if used.
|
|
1394
|
-
if tag != 0:
|
|
1395
|
-
len_ = buff.getUInt8()
|
|
1396
|
-
buff.position = buff.position + len_
|
|
1397
|
-
elif tag2 == XDLMSAPDU.CONFIRMED_SERVICE_ERROR:
|
|
1398
|
-
raise GXDLMSConfirmedServiceError(ConfirmedServiceError(buff.getUInt8()), ServiceError(buff.getUInt8()), buff.getUInt8())
|
|
1399
|
-
else:
|
|
1400
|
-
raise ValueError("Invalid tag.")
|
|
1401
|
-
# Get DLMS version number.
|
|
1402
|
-
if not response:
|
|
1403
|
-
self.settings.dlmsVersion = buff.getUInt8()
|
|
1404
|
-
if self.settings.dlmsVersion != 6:
|
|
1405
|
-
if not self.settings.isServer:
|
|
1406
|
-
raise ValueError("Invalid DLMS version number.")
|
|
1407
|
-
else:
|
|
1408
|
-
if buff.getUInt8() != 6:
|
|
1409
|
-
raise ValueError("Invalid DLMS version number.")
|
|
1410
|
-
# Tag for conformance block
|
|
1411
|
-
tag = buff.getUInt8()
|
|
1412
|
-
if tag != 0x5F:
|
|
1413
|
-
raise ValueError("Invalid tag.")
|
|
1414
|
-
# Old Way...
|
|
1415
|
-
if buff.getUInt8(buff.position) == 0x1F:
|
|
1416
|
-
buff.getUInt8()
|
|
1417
|
-
len_ = buff.getUInt8()
|
|
1418
|
-
# The number of unused bits in the bit string.
|
|
1419
|
-
tag = buff.getUInt8()
|
|
1420
|
-
#getConformanceToArray todo: make better
|
|
1421
|
-
v = _GXCommon.swapBits(buff.getUInt8())
|
|
1422
|
-
v |= _GXCommon.swapBits(buff.getUInt8()) << 8
|
|
1423
|
-
v |= _GXCommon.swapBits(buff.getUInt8()) << 16
|
|
1424
|
-
if self.settings.isServer:
|
|
1425
|
-
self.negotiated_conformance.set(v & self.settings.proposedConformance)
|
|
1426
|
-
else:
|
|
1427
|
-
self.negotiated_conformance.set(v)
|
|
1428
|
-
self.log(logL.INFO, f"SET CONFORMANCE: {self.negotiated_conformance}")
|
|
1429
|
-
if not response:
|
|
1430
|
-
# Proposed max PDU size.
|
|
1431
|
-
pdu = buff.getUInt16()
|
|
1432
|
-
self.settings.maxPduSize = pdu
|
|
1433
|
-
# If client asks too high PDU.
|
|
1434
|
-
if pdu > self.settings.maxServerPDUSize:
|
|
1435
|
-
self.settings.setMaxPduSize = self.settings.maxServerPDUSize
|
|
1436
|
-
else:
|
|
1437
|
-
pdu = buff.getUInt16()
|
|
1438
|
-
if pdu < 64:
|
|
1439
|
-
raise GXDLMSConfirmedServiceError(ConfirmedServiceError.INITIATE_ERROR, ServiceError.SERVICE, Service.PDU_SIZE)
|
|
1440
|
-
# Max PDU size.
|
|
1441
|
-
self.settings.maxPduSize = pdu
|
|
1442
|
-
if response:
|
|
1443
|
-
# VAA Name
|
|
1444
|
-
tag = buff.getUInt16()
|
|
1445
|
-
if tag == 0x0007:
|
|
1446
|
-
if not self.settings.getUseLogicalNameReferencing():
|
|
1447
|
-
raise ValueError("Invalid VAA.")
|
|
1448
|
-
elif tag == 0xFA00:
|
|
1449
|
-
# If SN
|
|
1450
|
-
if self.settings.getUseLogicalNameReferencing():
|
|
1451
|
-
raise ValueError("Invalid VAA.")
|
|
1452
|
-
else:
|
|
1453
|
-
# Unknown VAA.
|
|
1454
|
-
raise ValueError("Invalid VAA.")
|
|
1455
|
-
except Exception:
|
|
1456
|
-
raise GXDLMSException(AssociationResult.REJECTED_PERMANENT, AcseServiceUser.NO_REASON_GIVEN)
|
|
1457
|
-
elif tag == BerType.CONTEXT | AARQapdu.PROTOCOL_VERSION: # 0x80
|
|
1458
|
-
buff.getUInt8()
|
|
1459
|
-
unusedBits = buff.getUInt8()
|
|
1460
|
-
value = buff.getUInt8()
|
|
1461
|
-
sb = _GXCommon.toBitString(value, 8 - unusedBits)
|
|
1462
|
-
self.settings.protocolVersion = sb
|
|
1463
|
-
else:
|
|
1464
|
-
# Unknown tags.
|
|
1465
|
-
self.log(logL.DEB, "Unknown tag: " + str(tag) + ".")
|
|
1466
|
-
if buff.position < len(buff):
|
|
1467
|
-
len_ = buff.getUInt8()
|
|
1468
|
-
buff.position = buff.position + len_
|
|
1469
|
-
# All meters don't send user-information if connection is failed.
|
|
1470
|
-
# For this reason result component is check again.
|
|
1471
|
-
# if resultComponent != AssociationResult.ACCEPTED and resultDiagnosticValue != SourceDiagnostic.NONE:
|
|
1472
|
-
# raise exc.AssociationResultError(resultComponent, resultDiagnosticValue)
|
|
1473
|
-
return resultDiagnosticValue
|
|
1474
|
-
|
|
1475
|
-
def parseAareResponse(self, pdu: bytes) -> AcseServiceUser:
|
|
1476
|
-
""" TODO: need refactoring. Parses the AARE response. Parse method will update the following data: DLMSVersion, MaxReceivePDUSize, UseLogicalNameReferencing, LNSettings or SNSettings,
|
|
1477
|
-
LNSettings or SNSettings will be updated, depending on the referencing, Logical name or Short name.
|
|
1478
|
-
Received data. GXDLMSClient#aarqRequest GXDLMSClient#useLogicalNameReferencing GXDLMSClient#negotiatedConformance GXDLMSClient#proposedConformance """
|
|
1479
|
-
if (ret := self.parseAARE(pdu)) != AcseServiceUser.AUTHENTICATION_REQUIRED:
|
|
1480
|
-
self.level |= OSI.APPLICATION
|
|
1481
|
-
if self.settings.dlmsVersion != 6:
|
|
1482
|
-
raise ValueError("Invalid DLMS version number.")
|
|
1483
|
-
return ret
|
|
1484
|
-
|
|
1485
|
-
def generate_user_information(self, cipher, encryptedData) -> bytes:
|
|
1486
|
-
info = pack('B', BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.USER_INFORMATION)
|
|
1487
|
-
if not cipher or not cipher.isCiphered():
|
|
1488
|
-
# Length for AARQ user field + oding the choice for user-information (Octet STRING, universal)
|
|
1489
|
-
info += b'\x10\x04'
|
|
1490
|
-
i_r: bytes = self.getInitiateRequest()
|
|
1491
|
-
info += pack(F'B{len(i_r)}s', len(i_r), i_r)
|
|
1492
|
-
else:
|
|
1493
|
-
if encryptedData:
|
|
1494
|
-
# Length for AARQ user field
|
|
1495
|
-
info += pack('B', 4 + len(encryptedData))
|
|
1496
|
-
# Tag
|
|
1497
|
-
info += pack('B', BerType.OCTET_STRING)
|
|
1498
|
-
info += pack('B', 2 + len(encryptedData))
|
|
1499
|
-
# Coding the choice for user-information (Octet STRING,
|
|
1500
|
-
# universal)
|
|
1501
|
-
info += pack('B', XDLMSAPDU.GLO_INITIATE_REQUEST)
|
|
1502
|
-
info += pack('B', len(encryptedData))
|
|
1503
|
-
info += pack(F'{len(encryptedData)}s', encryptedData)
|
|
1504
|
-
else:
|
|
1505
|
-
tmp: bytes = self.getInitiateRequest()
|
|
1506
|
-
p = AesGcmParameter(XDLMSAPDU.GLO_INITIATE_REQUEST, cipher.systemTitle, cipher.blockCipherKey, cipher.authenticationKey)
|
|
1507
|
-
p.security = cipher.security
|
|
1508
|
-
p.invocationCounter = cipher.invocationCounter
|
|
1509
|
-
crypted = bytes(GXCiphering.encrypt(p, tmp))
|
|
1510
|
-
# Length for AARQ user field. Coding the choice for user-information (Octet string, universal)
|
|
1511
|
-
info += pack(F'BBB{len(crypted)}s',
|
|
1512
|
-
2 + len(crypted),
|
|
1513
|
-
BerType.OCTET_STRING,
|
|
1514
|
-
len(crypted),
|
|
1515
|
-
crypted)
|
|
1516
|
-
return info
|
|
1517
|
-
|
|
1518
|
-
def getInitiateRequest(self) -> bytes:
|
|
1519
|
-
"""DLMS UA 1000-2 Ed. 10. 11 AARQ and AARE encoding examples. 11.2 Encoding of the xDLMS InitiateRequest. Todo: rewrite with use UsefullTypes"""
|
|
1520
|
-
info = pack('B', XDLMSAPDU.INITIATE_REQUEST)
|
|
1521
|
-
if not self.settings.cipher or not self.settings.cipher.dedicatedKey:
|
|
1522
|
-
info += b'\x00'
|
|
1523
|
-
else:
|
|
1524
|
-
info += b'\x01' + cdt.encode_length(len(self.settings.cipher.dedicatedKey)) + bytes(self.settings.cipher.dedicatedKey)
|
|
1525
|
-
info += pack(
|
|
1526
|
-
">3B4s3sH",
|
|
1527
|
-
0, # encoding of the response-allowed component (BOOLEAN DEFAULT TRUE) usage flag (FALSE, default value TRUE conveyed)
|
|
1528
|
-
self.quality_of_service,
|
|
1529
|
-
self._objects.dlms_ver if self._objects else self.DEF_DLMS_VER,
|
|
1530
|
-
b'\x5f\x1f\x04\x00', # <5f1f> Tag for conformance block + <04>length of the conformance block + <00> encoding the number of unused bits in the bit string
|
|
1531
|
-
self.proposed_conformance.contents,
|
|
1532
|
-
self.receive_pdu_size)
|
|
1533
|
-
return info
|
|
1534
|
-
|
|
1535
|
-
def aarqRequest(self, m_id: mechanism_id.MechanismIdElement):
|
|
1536
|
-
""" Generate AARQ request. Because all_ meters can't read all_ data in one packet, the packet must be split first, by using SplitDataToPackets method. AARQ request as
|
|
1537
|
-
byte array. @see GXDLMSClient#parseAareResponse """
|
|
1538
|
-
info = bytes()
|
|
1539
|
-
self.settings.resetBlockIndex()
|
|
1540
|
-
self.settings.setStoCChallenge(None)
|
|
1541
|
-
# if self.auto_increase_invoke_ID:
|
|
1542
|
-
# self.settings.setInvokeID(0)
|
|
1543
|
-
# else:
|
|
1544
|
-
# self.settings.setInvokeID(1)
|
|
1545
|
-
# If authentication or ciphering is used.
|
|
1546
|
-
# ProtocolVersion: BerType.CONTEXT | AARQ-apdu.PROTOCOL_VERSION + length(always 2) + unused bites + context
|
|
1547
|
-
if self.protocol_version.encoding != b'\x04\x01\x80':
|
|
1548
|
-
info += pack('2sBc', b'\x80\x02',
|
|
1549
|
-
8 - len(self.protocol_version),
|
|
1550
|
-
self.protocol_version.contents)
|
|
1551
|
-
# Application context name tag. Where A1 - Tag, 09 - content name length, 06 - BerType.OBJECT_IDENTIFIER, 07 - info length
|
|
1552
|
-
info += b'\xa1\x09\x06\x07' + self.APP_CONTEXT_NAME.contents
|
|
1553
|
-
# Add system title.
|
|
1554
|
-
ciphered = self.settings.cipher and self.settings.cipher.isCiphered()
|
|
1555
|
-
if not self.settings.isServer and (ciphered or m_id == mechanism_id.HIGH_GMAC) or m_id == mechanism_id.HIGH_ECDSA:
|
|
1556
|
-
if len(self.settings.cipher.systemTitle) != 8:
|
|
1557
|
-
raise ValueError("SystemTitle")
|
|
1558
|
-
# Add calling-AP-title: BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.CALLING_AP_TITLE + length + BerType.OCTET_STRING + length + systemTitle
|
|
1559
|
-
info += pack(F'cBcB{len(self.settings.cipher.systemTitle)}s',
|
|
1560
|
-
b'\xa6',
|
|
1561
|
-
2 + len(self.settings.cipher.systemTitle),
|
|
1562
|
-
b'\x04',
|
|
1563
|
-
len(self.settings.cipher.systemTitle),
|
|
1564
|
-
self.settings.cipher.systemTitle)
|
|
1565
|
-
# CallingAEInvocationId: BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.CALLING_AE_INVOCATION_ID + length + BerType.INTEGER + length + userId
|
|
1566
|
-
if not self.settings.isServer and self.settings.userId != -1:
|
|
1567
|
-
info += pack(F'4sB',
|
|
1568
|
-
b'\xa9\x03\x02\x01',
|
|
1569
|
-
self.settings.userId)
|
|
1570
|
-
# Retrieves the string that indicates the level of authentication, if any.
|
|
1571
|
-
if m_id != mechanism_id.NONE or (self.settings.cipher and self.settings.cipher.security != Security.NONE):
|
|
1572
|
-
info += b'\x8a\x02\x07\x80'
|
|
1573
|
-
# Where: 8b - Tag(CONTEXT(0x80) + AARQ-apdu.MECHANISM_NAME(0x0b)), 07 - info length
|
|
1574
|
-
info += b'\x8b\x07' + AuthenticationMechanismName.get_AARQ_mechanism_name(
|
|
1575
|
-
cryptographic=2,
|
|
1576
|
-
algorithm_id=int(m_id))
|
|
1577
|
-
# Add Calling authentication information.
|
|
1578
|
-
if m_id != mechanism_id.NONE:
|
|
1579
|
-
if m_id == mechanism_id.LOW:
|
|
1580
|
-
c_a_v = self.secret
|
|
1581
|
-
""" calling-authentication-value """
|
|
1582
|
-
elif m_id == mechanism_id.HIGH:
|
|
1583
|
-
self.settings.ctoSChallenge = os.urandom(16)
|
|
1584
|
-
c_a_v = self.settings.ctoSChallenge
|
|
1585
|
-
else:
|
|
1586
|
-
# TODO: must be 8..64 bytes length of urandom for different auth level
|
|
1587
|
-
self.settings.ctoSChallenge = os.urandom(16)
|
|
1588
|
-
c_a_v = self.settings.ctoSChallenge
|
|
1589
|
-
# BerType.CONTEXT | BerType.CONSTRUCTED | AARQ-apdu.CALLING_AUTHENTICATION_VALUE + length + context + info_len
|
|
1590
|
-
info += pack(F'cBBB{len(c_a_v)}s',
|
|
1591
|
-
b'\xac',
|
|
1592
|
-
2 + len(c_a_v),
|
|
1593
|
-
BerType.CONTEXT,
|
|
1594
|
-
len(c_a_v),
|
|
1595
|
-
c_a_v)
|
|
1596
|
-
u_i = self.generate_user_information(self.settings.cipher, None)
|
|
1597
|
-
info = pack('BB', BerType.APPLICATION | BerType.CONSTRUCTED,
|
|
1598
|
-
len(info + u_i)) + info + u_i
|
|
1599
|
-
p = GXDLMSLNParameters(self.settings, 0, ACSEAPDU.AARQ, 0, info, None, 0xff)
|
|
1600
|
-
return self.getLnMessages(p)
|
|
1601
|
-
|
|
1602
|
-
def getLnMessages(self, p: GXDLMSLNParameters):
|
|
1603
|
-
reply = GXByteBuffer()
|
|
1604
|
-
messages = []
|
|
1605
|
-
frame_ = 0
|
|
1606
|
-
if (
|
|
1607
|
-
p.command == XDLMSAPDU.DATA_NOTIFICATION
|
|
1608
|
-
or p.command == XDLMSAPDU.EVENT_NOTIFICATION_REQUEST
|
|
1609
|
-
):
|
|
1610
|
-
frame_ = 0x13
|
|
1611
|
-
while True:
|
|
1612
|
-
# """ Get next logical name PDU. @param p LN parameters. @param reply Generated message. """
|
|
1613
|
-
ciphering = (
|
|
1614
|
-
p.command != ACSEAPDU.AARQ
|
|
1615
|
-
and p.command != ACSEAPDU.AARE
|
|
1616
|
-
and self.settings.cipher
|
|
1617
|
-
and self.settings.cipher.security != Security.NONE
|
|
1618
|
-
)
|
|
1619
|
-
len_ = 0
|
|
1620
|
-
if p.command == ACSEAPDU.AARQ:
|
|
1621
|
-
if (
|
|
1622
|
-
self.settings.gateway
|
|
1623
|
-
and self.settings.gateway.physicalDeviceAddress
|
|
1624
|
-
):
|
|
1625
|
-
reply.setUInt8(XDLMSAPDU.GATEWAY_REQUEST)
|
|
1626
|
-
reply.setUInt8(self.settings.gateway.networkId)
|
|
1627
|
-
reply.setUInt8(len(self.settings.gateway.physicalDeviceAddress))
|
|
1628
|
-
reply.set(self.settings.gateway.physicalDeviceAddress)
|
|
1629
|
-
reply.set(p.attributeDescriptor)
|
|
1630
|
-
else:
|
|
1631
|
-
if p.command != XDLMSAPDU.GENERAL_BLOCK_TRANSFER:
|
|
1632
|
-
reply.setUInt8(p.command)
|
|
1633
|
-
if p.command in (XDLMSAPDU.EVENT_NOTIFICATION_REQUEST, XDLMSAPDU.DATA_NOTIFICATION, XDLMSAPDU.ACCESS_REQUEST, XDLMSAPDU.ACCESS_RESPONSE):
|
|
1634
|
-
if p.command != XDLMSAPDU.EVENT_NOTIFICATION_REQUEST:
|
|
1635
|
-
if p.invokeId != 0:
|
|
1636
|
-
reply.setUInt32(p.invokeId)
|
|
1637
|
-
else:
|
|
1638
|
-
reply.setUInt32(GXDLMS.getLongInvokeIDPriority(self.settings))
|
|
1639
|
-
if p.time is None:
|
|
1640
|
-
reply.setUInt8(cdt.NullData.TAG)
|
|
1641
|
-
else:
|
|
1642
|
-
pos = len(reply)
|
|
1643
|
-
_GXCommon.setData(self.settings, reply, cdt.OctetString.TAG, p.getTime())
|
|
1644
|
-
if p.command != XDLMSAPDU.EVENT_NOTIFICATION_REQUEST:
|
|
1645
|
-
reply.move(pos + 1, pos, len(reply) - pos - 1)
|
|
1646
|
-
GXDLMS.multipleBlocks(p, reply, ciphering)
|
|
1647
|
-
elif p.command != ACSEAPDU.RLRQ:
|
|
1648
|
-
if (
|
|
1649
|
-
p.command != XDLMSAPDU.GET_REQUEST
|
|
1650
|
-
and p.data
|
|
1651
|
-
and reply
|
|
1652
|
-
):
|
|
1653
|
-
GXDLMS.multipleBlocks(p, reply, ciphering)
|
|
1654
|
-
if p.command == XDLMSAPDU.SET_REQUEST:
|
|
1655
|
-
if (
|
|
1656
|
-
p.multipleBlocks
|
|
1657
|
-
and not self.negotiated_conformance.general_block_transfer
|
|
1658
|
-
):
|
|
1659
|
-
if p.requestType == 1:
|
|
1660
|
-
p.requestType = SetRequest.SET_REQUEST_FIRST_DATABLOCK
|
|
1661
|
-
elif p.requestType == 2:
|
|
1662
|
-
p.requestType = SetRequest.SET_REQUEST_WITH_DATABLOCK
|
|
1663
|
-
if p.command == XDLMSAPDU.GET_RESPONSE:
|
|
1664
|
-
if (
|
|
1665
|
-
p.multipleBlocks
|
|
1666
|
-
and not self.negotiated_conformance.general_block_transfer
|
|
1667
|
-
):
|
|
1668
|
-
if p.requestType == 1:
|
|
1669
|
-
p.requestType = 2
|
|
1670
|
-
if p.command != XDLMSAPDU.GENERAL_BLOCK_TRANSFER:
|
|
1671
|
-
reply.setUInt8(p.requestType)
|
|
1672
|
-
if p.invokeId != 0:
|
|
1673
|
-
reply.setUInt8(p.invokeId)
|
|
1674
|
-
else:
|
|
1675
|
-
reply.setUInt8(GXDLMS.getInvokeIDPriority(self.settings))
|
|
1676
|
-
reply.set(p.attributeDescriptor)
|
|
1677
|
-
if (
|
|
1678
|
-
self.settings.is_multiple_block()
|
|
1679
|
-
and self.negotiated_conformance.general_block_transfer
|
|
1680
|
-
):
|
|
1681
|
-
if p.lastBlock:
|
|
1682
|
-
reply.setUInt8(1)
|
|
1683
|
-
self.settings.setCount(0)
|
|
1684
|
-
self.settings.setIndex(0)
|
|
1685
|
-
else:
|
|
1686
|
-
reply.setUInt8(0)
|
|
1687
|
-
reply.setUInt32(p.blockIndex)
|
|
1688
|
-
p.blockIndex += 1
|
|
1689
|
-
if p.status != 0xFF:
|
|
1690
|
-
if (
|
|
1691
|
-
p.status != 0
|
|
1692
|
-
and p.command == XDLMSAPDU.GET_RESPONSE
|
|
1693
|
-
):
|
|
1694
|
-
reply.setUInt8(1)
|
|
1695
|
-
reply.setUInt8(p.status)
|
|
1696
|
-
if p.data:
|
|
1697
|
-
len_ = p.data.size - p.data.position
|
|
1698
|
-
else:
|
|
1699
|
-
len_ = 0
|
|
1700
|
-
totalLength = len_ + len(reply)
|
|
1701
|
-
if ciphering:
|
|
1702
|
-
totalLength += GXDLMS._CIPHERING_HEADER_SIZE
|
|
1703
|
-
if totalLength > self.settings.maxPduSize:
|
|
1704
|
-
len_ = self.settings.maxPduSize - len(reply)
|
|
1705
|
-
if ciphering:
|
|
1706
|
-
len_ -= GXDLMS._CIPHERING_HEADER_SIZE
|
|
1707
|
-
len_ -= _GXCommon.getObjectCountSizeInBytes(len_)
|
|
1708
|
-
_GXCommon.setObjectCount(len_, reply)
|
|
1709
|
-
reply.set(p.data, len_)
|
|
1710
|
-
if len_ == 0:
|
|
1711
|
-
if (
|
|
1712
|
-
p.status != 0xFF
|
|
1713
|
-
and p.command != XDLMSAPDU.GENERAL_BLOCK_TRANSFER
|
|
1714
|
-
):
|
|
1715
|
-
if (
|
|
1716
|
-
p.status != 0
|
|
1717
|
-
and p.command == XDLMSAPDU.GET_RESPONSE
|
|
1718
|
-
):
|
|
1719
|
-
reply.setUInt8(1)
|
|
1720
|
-
reply.setUInt8(p.status)
|
|
1721
|
-
if p.data:
|
|
1722
|
-
len_ = p.data.size - p.data.position
|
|
1723
|
-
if self.settings.gateway and self.settings.gateway.physicalDeviceAddress:
|
|
1724
|
-
if 3 + len_ + len(self.settings.gateway.physicalDeviceAddress) > self.settings.maxPduSize:
|
|
1725
|
-
len_ -= (3 + len(self.settings.gateway.physicalDeviceAddress))
|
|
1726
|
-
tmp = GXByteBuffer(reply)
|
|
1727
|
-
reply.size = 0
|
|
1728
|
-
reply.setUInt8(XDLMSAPDU.GATEWAY_REQUEST)
|
|
1729
|
-
reply.setUInt8(self.settings.gateway.networkId)
|
|
1730
|
-
reply.setUInt8(len(self.settings.gateway.physicalDeviceAddress))
|
|
1731
|
-
reply.set(self.settings.gateway.physicalDeviceAddress)
|
|
1732
|
-
reply.set(tmp)
|
|
1733
|
-
if self.negotiated_conformance.general_block_transfer:
|
|
1734
|
-
if 7 + len_ + len(reply) > self.settings.maxPduSize:
|
|
1735
|
-
len_ = self.settings.maxPduSize - len(reply) - 7
|
|
1736
|
-
if (
|
|
1737
|
-
ciphering
|
|
1738
|
-
and p.command != XDLMSAPDU.GENERAL_BLOCK_TRANSFER
|
|
1739
|
-
):
|
|
1740
|
-
reply.set(p.data)
|
|
1741
|
-
tmp = []
|
|
1742
|
-
if self.settings.cipher.securitySuite == SecuritySuite.AES_GCM_128_AUT_ENCR_AND_AES_128_KEY_WRAP:
|
|
1743
|
-
tmp = self.cipher0(p, reply)
|
|
1744
|
-
p.data.size = 0
|
|
1745
|
-
p.data.set(tmp)
|
|
1746
|
-
reply.size = 0
|
|
1747
|
-
len_ = p.data.size
|
|
1748
|
-
if 7 + len_ > self.settings.maxPduSize:
|
|
1749
|
-
len_ = self.settings.maxPduSize - 7
|
|
1750
|
-
ciphering = False
|
|
1751
|
-
elif (
|
|
1752
|
-
p.command != XDLMSAPDU.GET_REQUEST
|
|
1753
|
-
and len_ + len(reply) > self.settings.maxPduSize
|
|
1754
|
-
):
|
|
1755
|
-
len_ = self.settings.maxPduSize - len(reply)
|
|
1756
|
-
reply.set(p.data, p.data.position, len_)
|
|
1757
|
-
elif (
|
|
1758
|
-
(
|
|
1759
|
-
self.settings.gateway
|
|
1760
|
-
and self.settings.gateway.physicalDeviceAddress
|
|
1761
|
-
)
|
|
1762
|
-
and not (
|
|
1763
|
-
p.command == XDLMSAPDU.GENERAL_BLOCK_TRANSFER
|
|
1764
|
-
or (
|
|
1765
|
-
p.multipleBlocks
|
|
1766
|
-
and self.negotiated_conformance.general_block_transfer
|
|
1767
|
-
)
|
|
1768
|
-
)
|
|
1769
|
-
):
|
|
1770
|
-
if 3 + len_ + len(self.settings.gateway.physicalDeviceAddress) > self.settings.maxPduSize:
|
|
1771
|
-
len_ -= (3 + len(self.settings.gateway.physicalDeviceAddress))
|
|
1772
|
-
tmp = GXByteBuffer(reply)
|
|
1773
|
-
reply.size = 0
|
|
1774
|
-
reply.setUInt8(XDLMSAPDU.GATEWAY_REQUEST)
|
|
1775
|
-
reply.setUInt8(self.settings.gateway.networkId)
|
|
1776
|
-
reply.setUInt8(len(self.settings.gateway.physicalDeviceAddress))
|
|
1777
|
-
reply.set(self.settings.gateway.physicalDeviceAddress)
|
|
1778
|
-
reply.set(tmp)
|
|
1779
|
-
if (
|
|
1780
|
-
ciphering
|
|
1781
|
-
and reply
|
|
1782
|
-
and not self.negotiated_conformance.general_block_transfer
|
|
1783
|
-
and p.command != XDLMSAPDU.RELEASE_REQUEST
|
|
1784
|
-
):
|
|
1785
|
-
tmp = []
|
|
1786
|
-
if self.settings.cipher.securitySuite == SecuritySuite.AES_GCM_128_AUT_ENCR_AND_AES_128_KEY_WRAP:
|
|
1787
|
-
tmp = self.cipher0(p, reply.array())
|
|
1788
|
-
reply.size = 0
|
|
1789
|
-
reply.set(tmp)
|
|
1790
|
-
if (
|
|
1791
|
-
p.command == XDLMSAPDU.GENERAL_BLOCK_TRANSFER
|
|
1792
|
-
or (
|
|
1793
|
-
p.multipleBlocks
|
|
1794
|
-
and self.negotiated_conformance.general_block_transfer
|
|
1795
|
-
)
|
|
1796
|
-
):
|
|
1797
|
-
bb = GXByteBuffer()
|
|
1798
|
-
bb.set(reply)
|
|
1799
|
-
reply.clear()
|
|
1800
|
-
reply.setUInt8(XDLMSAPDU.GENERAL_BLOCK_TRANSFER)
|
|
1801
|
-
if p.lastBlock:
|
|
1802
|
-
value = 0x80
|
|
1803
|
-
elif p.streaming:
|
|
1804
|
-
value = 0x40
|
|
1805
|
-
else:
|
|
1806
|
-
value = 0
|
|
1807
|
-
value |= p.windowSize
|
|
1808
|
-
reply.setUInt8(value)
|
|
1809
|
-
reply.setUInt16(p.blockIndex)
|
|
1810
|
-
p.blockIndex += 1
|
|
1811
|
-
if (
|
|
1812
|
-
p.command != XDLMSAPDU.DATA_NOTIFICATION
|
|
1813
|
-
and p.blockNumberAck != 0
|
|
1814
|
-
):
|
|
1815
|
-
reply.setUInt16(p.blockNumberAck)
|
|
1816
|
-
p.blockNumberAck += 1
|
|
1817
|
-
else:
|
|
1818
|
-
p.blockNumberAck = -1
|
|
1819
|
-
reply.setUInt16(0)
|
|
1820
|
-
_GXCommon.setObjectCount(len(bb), reply)
|
|
1821
|
-
reply.set(bb)
|
|
1822
|
-
if p.command != XDLMSAPDU.GENERAL_BLOCK_TRANSFER:
|
|
1823
|
-
p.command = XDLMSAPDU.GENERAL_BLOCK_TRANSFER
|
|
1824
|
-
p.blockNumberAck += 1
|
|
1825
|
-
if (
|
|
1826
|
-
self.settings.gateway
|
|
1827
|
-
and self.settings.gateway.physicalDeviceAddress
|
|
1828
|
-
):
|
|
1829
|
-
if 3 + len_ + len(self.settings.gateway.physicalDeviceAddress) > self.settings.maxPduSize:
|
|
1830
|
-
len_ -= (3 + len(self.settings.gateway.physicalDeviceAddress))
|
|
1831
|
-
tmp = GXByteBuffer(reply)
|
|
1832
|
-
reply.size = 0
|
|
1833
|
-
reply.setUInt8(XDLMSAPDU.GATEWAY_REQUEST)
|
|
1834
|
-
reply.setUInt8(self.settings.gateway.networkId)
|
|
1835
|
-
reply.setUInt8(len(self.settings.gateway.physicalDeviceAddress))
|
|
1836
|
-
reply.set(self.settings.gateway.physicalDeviceAddress)
|
|
1837
|
-
reply.set(tmp)
|
|
1838
|
-
p.lastBlock = True
|
|
1839
|
-
if p.attributeDescriptor is None:
|
|
1840
|
-
self.settings.increaseBlockIndex()
|
|
1841
|
-
if (
|
|
1842
|
-
p.command == ACSEAPDU.AARQ
|
|
1843
|
-
and p.command == XDLMSAPDU.GET_REQUEST
|
|
1844
|
-
):
|
|
1845
|
-
assert not self.settings.maxPduSize < len(reply)
|
|
1846
|
-
match self.com_profile:
|
|
1847
|
-
case c_pf.TCPUDPIP():
|
|
1848
|
-
messages.append(GXDLMS.getWrapperFrame(self.settings, p.command, reply)) # TODO: rewrite getWrapperFrame with return list[bytes]
|
|
1849
|
-
case c_pf.HDLC():
|
|
1850
|
-
self.add_frames_to_queue(frame.Control(frame_), bytes(reply.array()))
|
|
1851
|
-
case _:
|
|
1852
|
-
raise ValueError("InterfaceType")
|
|
1853
|
-
reply.clear()
|
|
1854
|
-
frame_ = 0
|
|
1855
|
-
if (
|
|
1856
|
-
not p.data
|
|
1857
|
-
or p.data.position == p.data.size
|
|
1858
|
-
):
|
|
1859
|
-
break
|
|
1860
|
-
return messages
|
|
1861
|
-
|
|
1862
|
-
def get_get_request_normal(self, attr_desc: ut.CosemAttributeDescriptor | ut.CosemAttributeDescriptorWithSelection):
|
|
1863
|
-
p = GXDLMSLNParameters(settings=self.settings,
|
|
1864
|
-
invokeId=0,
|
|
1865
|
-
command=XDLMSAPDU.GET_REQUEST,
|
|
1866
|
-
requestType=pdu.GetResponse.NORMAL,
|
|
1867
|
-
attributeDescriptor=GXByteBuffer(attr_desc.contents),
|
|
1868
|
-
data=None,
|
|
1869
|
-
status=0xFF)
|
|
1870
|
-
return self.getLnMessages(p)
|
|
1871
|
-
|
|
1872
|
-
def get_set_request_normal(self, obj: ic.COSEMInterfaceClasses, attr_index: int, value: bytes = None):
|
|
1873
|
-
self.settings.resetBlockIndex()
|
|
1874
|
-
access_selection_parameters = b'\x00'
|
|
1875
|
-
attribute_descriptor = GXByteBuffer(obj.get_attribute_descriptor(attr_index) + access_selection_parameters)
|
|
1876
|
-
data = GXByteBuffer()
|
|
1877
|
-
if value:
|
|
1878
|
-
data.set(value)
|
|
1879
|
-
else:
|
|
1880
|
-
attr = obj.get_attr(attr_index)
|
|
1881
|
-
data.set(attr.encoding) # add raw data
|
|
1882
|
-
p = GXDLMSLNParameters(self.settings, 0, XDLMSAPDU.SET_REQUEST, SetRequest.SET_REQUEST_NORMAL, attribute_descriptor, data, 0xff)
|
|
1883
|
-
p.blockIndex = self.settings.blockIndex
|
|
1884
|
-
p.blockNumberAck = self.settings.blockNumberAck
|
|
1885
|
-
p.streaming = False
|
|
1886
|
-
return self.getLnMessages(p)
|
|
1887
|
-
|
|
1888
|
-
def get_set_request_normal2(self, attr_desc: ut.CosemAttributeDescriptor, value: cdt.CommonDataTypes):
|
|
1889
|
-
self.settings.resetBlockIndex()
|
|
1890
|
-
attribute_descriptor = GXByteBuffer(attr_desc.contents)
|
|
1891
|
-
data = GXByteBuffer(value.encoding)
|
|
1892
|
-
p = GXDLMSLNParameters(self.settings, 0, XDLMSAPDU.SET_REQUEST, SetRequest.SET_REQUEST_NORMAL, attribute_descriptor, data, 0xff)
|
|
1893
|
-
p.blockIndex = self.settings.blockIndex
|
|
1894
|
-
p.blockNumberAck = self.settings.blockNumberAck
|
|
1895
|
-
p.streaming = False
|
|
1896
|
-
return self.getLnMessages(p)
|
|
1897
|
-
|
|
1898
|
-
@deprecated("use get_action_request_normal")
|
|
1899
|
-
def get_action_request_normal_old(self, meth_desc: ut.CosemMethodDescriptor):
|
|
1900
|
-
self.settings.resetBlockIndex()
|
|
1901
|
-
method = self.objects.get_object(meth_desc).get_meth(int(meth_desc.method_id))
|
|
1902
|
-
method_invocation_parameters = GXByteBuffer(cdt.Boolean(b'\x03' + method.TAG).contents + method.encoding)
|
|
1903
|
-
method_descriptor = GXByteBuffer(meth_desc.contents)
|
|
1904
|
-
p = GXDLMSLNParameters(self.settings, 0, XDLMSAPDU.ACTION_REQUEST, ActionRequest.NORMAL, method_descriptor, method_invocation_parameters, 0xff)
|
|
1905
|
-
return self.getLnMessages(p)
|
|
1906
|
-
|
|
1907
|
-
def get_action_request_normal(self, meth_desc: ut.CosemMethodDescriptor, method: cdt.CommonDataType):
|
|
1908
|
-
"""method: specific method"""
|
|
1909
|
-
self.settings.resetBlockIndex()
|
|
1910
|
-
method_invocation_parameters = GXByteBuffer(cdt.Boolean(b'\x03' + method.TAG).contents + method.encoding)
|
|
1911
|
-
method_descriptor = GXByteBuffer(meth_desc.contents)
|
|
1912
|
-
p = GXDLMSLNParameters(self.settings, 0, XDLMSAPDU.ACTION_REQUEST, ActionRequest.NORMAL, method_descriptor, method_invocation_parameters, 0xff)
|
|
1913
|
-
return self.getLnMessages(p)
|
|
1914
|
-
|
|
1915
|
-
def releaseRequest(self):
|
|
1916
|
-
# TODO: rewrite
|
|
1917
|
-
info = b'\x03\x80\x01\x00'
|
|
1918
|
-
if self.use_protected_release:
|
|
1919
|
-
#Increase IC.
|
|
1920
|
-
if self.settings.cipher and self.settings.cipher.isCiphered:
|
|
1921
|
-
self.settings.cipher.invocationCounter = self.settings.cipher.invocationCounter + 1
|
|
1922
|
-
info += self.generate_user_information(self.settings.cipher, None)
|
|
1923
|
-
info = pack('H', len(info)) + info
|
|
1924
|
-
buff = GXByteBuffer(info)
|
|
1925
|
-
if self.settings.getUseLogicalNameReferencing():
|
|
1926
|
-
p = GXDLMSLNParameters(self.settings, 0, ACSEAPDU.RLRQ, 0, buff, None, 0xff)
|
|
1927
|
-
reply = self.getLnMessages(p)
|
|
1928
|
-
else:
|
|
1929
|
-
reply = self.getSnMessages(GXDLMSSNParameters(self.settings, ACSEAPDU.RLRQ, 0xFF, 0xFF, None, buff))
|
|
1930
|
-
self.level -= OSI.APPLICATION
|
|
1931
|
-
return reply
|
|
1932
|
-
|
|
1933
|
-
@classmethod
|
|
1934
|
-
def getGloMessage(cls, command: XDLMSAPDU | ACSEAPDU) -> XDLMSAPDU | ACSEAPDU:
|
|
1935
|
-
""" Get used glo message. Executed command. Integer value of glo message."""
|
|
1936
|
-
match command:
|
|
1937
|
-
case XDLMSAPDU.READ_REQUEST: return XDLMSAPDU.GLO_READ_REQUEST
|
|
1938
|
-
case XDLMSAPDU.GET_REQUEST: return XDLMSAPDU.GLO_GET_REQUEST
|
|
1939
|
-
case XDLMSAPDU.WRITE_REQUEST: return XDLMSAPDU.GLO_WRITE_REQUEST
|
|
1940
|
-
case XDLMSAPDU.SET_REQUEST: return XDLMSAPDU.GLO_SET_REQUEST
|
|
1941
|
-
case XDLMSAPDU.ACTION_REQUEST: return XDLMSAPDU.GLO_ACTION_REQUEST
|
|
1942
|
-
case XDLMSAPDU.READ_RESPONSE: return XDLMSAPDU.GLO_READ_RESPONSE
|
|
1943
|
-
case XDLMSAPDU.GET_RESPONSE: return XDLMSAPDU.GLO_GET_RESPONSE
|
|
1944
|
-
case XDLMSAPDU.WRITE_RESPONSE: return XDLMSAPDU.GLO_WRITE_RESPONSE
|
|
1945
|
-
case XDLMSAPDU.SET_RESPONSE: return XDLMSAPDU.GLO_SET_RESPONSE
|
|
1946
|
-
case XDLMSAPDU.ACTION_RESPONSE: return XDLMSAPDU.GLO_ACTION_RESPONSE
|
|
1947
|
-
case XDLMSAPDU.DATA_NOTIFICATION: return XDLMSAPDU.GENERAL_GLO_CIPHERING
|
|
1948
|
-
case ACSEAPDU.RLRQ: return ACSEAPDU.RLRQ
|
|
1949
|
-
case ACSEAPDU.RLRE: return ACSEAPDU.RLRE
|
|
1950
|
-
case _: raise Exception("Invalid GLO command.")
|
|
1951
|
-
|
|
1952
|
-
@classmethod
|
|
1953
|
-
def getDedMessage(cls, command: XDLMSAPDU | ACSEAPDU) -> XDLMSAPDU | ACSEAPDU:
|
|
1954
|
-
""" Get used ded message. Executed command. Integer value of ded message. """
|
|
1955
|
-
match command:
|
|
1956
|
-
case XDLMSAPDU.GET_REQUEST: return XDLMSAPDU.DED_GET_REQUEST
|
|
1957
|
-
case XDLMSAPDU.SET_REQUEST: return XDLMSAPDU.DED_SET_REQUEST
|
|
1958
|
-
case XDLMSAPDU.ACTION_REQUEST: return XDLMSAPDU.DED_ACTION_REQUEST
|
|
1959
|
-
case XDLMSAPDU.GET_RESPONSE: return XDLMSAPDU.DED_GET_RESPONSE
|
|
1960
|
-
case XDLMSAPDU.SET_RESPONSE: return XDLMSAPDU.DED_SET_RESPONSE
|
|
1961
|
-
case XDLMSAPDU.ACTION_RESPONSE: return XDLMSAPDU.DED_ACTION_RESPONSE
|
|
1962
|
-
case XDLMSAPDU.DATA_NOTIFICATION: return XDLMSAPDU.GENERAL_DED_CIPHERING
|
|
1963
|
-
case ACSEAPDU.RLRQ: return ACSEAPDU.RLRQ
|
|
1964
|
-
case ACSEAPDU.RLRE: return ACSEAPDU.RLRE
|
|
1965
|
-
case _: raise Exception("Invalid DED command.")
|
|
1966
|
-
|
|
1967
|
-
def cipher0(self, p: GXDLMSLNParameters, data: GXByteBuffer):
|
|
1968
|
-
cmd = 0
|
|
1969
|
-
key = None
|
|
1970
|
-
cipher = p.settings.cipher
|
|
1971
|
-
if not self.negotiated_conformance.general_protection:
|
|
1972
|
-
if cipher.dedicatedKey and (OSI.APPLICATION in self.level): # todo: maybe level is wrong
|
|
1973
|
-
cmd = self.getDedMessage(p.command)
|
|
1974
|
-
key = cipher.dedicatedKey
|
|
1975
|
-
else:
|
|
1976
|
-
cmd = self.getGloMessage(p.command)
|
|
1977
|
-
key = cipher.blockCipherKey
|
|
1978
|
-
else:
|
|
1979
|
-
if cipher.dedicatedKey:
|
|
1980
|
-
cmd = XDLMSAPDU.GENERAL_DED_CIPHERING
|
|
1981
|
-
key = cipher.dedicatedKey
|
|
1982
|
-
else:
|
|
1983
|
-
cmd = XDLMSAPDU.GENERAL_GLO_CIPHERING
|
|
1984
|
-
key = cipher.blockCipherKey
|
|
1985
|
-
cipher.invocationCounter = cipher.invocationCounter + 1
|
|
1986
|
-
s = AesGcmParameter(cmd, cipher.systemTitle, key, cipher.authenticationKey)
|
|
1987
|
-
s.ignoreSystemTitle = p.settings.standard == Standard.ITALY
|
|
1988
|
-
s.security = cipher.security
|
|
1989
|
-
s.invocationCounter = cipher.invocationCounter
|
|
1990
|
-
tmp = GXCiphering.encrypt(s, data)
|
|
1991
|
-
if p.command == XDLMSAPDU.DATA_NOTIFICATION or p.command == XDLMSAPDU.GENERAL_GLO_CIPHERING or p.command == XDLMSAPDU.GENERAL_DED_CIPHERING:
|
|
1992
|
-
reply = GXByteBuffer()
|
|
1993
|
-
reply.setUInt8(tmp[0])
|
|
1994
|
-
if p.settings.getStandard() == Standard.ITALY:
|
|
1995
|
-
reply.setUInt8(0)
|
|
1996
|
-
else:
|
|
1997
|
-
_GXCommon.setObjectCount(len(p.settings.cipher.systemTitle), reply)
|
|
1998
|
-
reply.set(p.settings.cipher.systemTitle)
|
|
1999
|
-
reply.set(tmp, 1, len(tmp))
|
|
2000
|
-
return reply.array()
|
|
2001
|
-
return tmp
|
|
2002
|
-
|
|
2003
|
-
@property
|
|
2004
|
-
def current_association(self) -> AssociationLN:
|
|
2005
|
-
return self.objects.sap2association(self.SAP)
|
|
2006
|
-
|
|
2007
|
-
def get_SNRM_request(self):
|
|
2008
|
-
""" Generates SNRM request. his method is used to generate send SNRMRequest. Before the SNRM request can be generated, at least the following properties must be set:
|
|
2009
|
-
ClientAddress, ServerAddress.
|
|
2010
|
-
According to IEC 62056-47: when communicating using TCP/IP, the SNRM request is not send. """
|
|
2011
|
-
self.add_frames_to_queue(control=frame.Control.SNRM_P)
|
|
2012
|
-
|
|
2013
|
-
def add_frames_to_queue(self, control: frame.Control, data: bytes = bytes()):
|
|
2014
|
-
""" Create and set new frames to queue """
|
|
2015
|
-
new_frames: Deque[frame.Frame] = deque()
|
|
2016
|
-
""" frames container """
|
|
2017
|
-
if control == frame.Control.SNRM_P:
|
|
2018
|
-
info = self.com_profile.negotiation.SNRM
|
|
2019
|
-
elif control.is_information():
|
|
2020
|
-
info = sub_layer.LLC(message=data).content
|
|
2021
|
-
""" HDLS info field """
|
|
2022
|
-
else:
|
|
2023
|
-
info = bytes()
|
|
2024
|
-
if len(data) != 0:
|
|
2025
|
-
raise ValueError('Warning DATA not empty, but frame not info')
|
|
2026
|
-
while True:
|
|
2027
|
-
info3 = info[:self.com_profile.negotiation.max_info_transmit]
|
|
2028
|
-
info = info[self.com_profile.negotiation.max_info_transmit:]
|
|
2029
|
-
new_frames.append(frame.Frame(control=control if control != 0 else self.settings.getNextSend(True),
|
|
2030
|
-
DA=self.DA,
|
|
2031
|
-
SA=self.SA,
|
|
2032
|
-
info=info3,
|
|
2033
|
-
is_segmentation=bool(len(info))
|
|
2034
|
-
))
|
|
2035
|
-
if len(info) == 0:
|
|
2036
|
-
break
|
|
2037
|
-
else:
|
|
2038
|
-
control = frame.Control(self.settings.getNextSend(False))
|
|
2039
|
-
self.send_frames.extend(new_frames)
|
|
2040
|
-
|
|
2041
|
-
def __str__(self):
|
|
2042
|
-
if not self._objects or not self._objects.LDN.value:
|
|
2043
|
-
return str(self.id)
|
|
2044
|
-
else:
|
|
2045
|
-
return self._objects.LDN.value.to_str()
|
|
2046
|
-
|
|
2047
|
-
def get_serial_number(self) -> str:
|
|
2048
|
-
""" return serial number as text. If serial object is absence return 'недоступен' """
|
|
2049
|
-
if self._objects is None:
|
|
2050
|
-
return "нет типа"
|
|
2051
|
-
obj = self._objects.serial_number
|
|
2052
|
-
if isinstance(obj, Data) and obj.value is not None:
|
|
2053
|
-
if isinstance(obj.value, cdt.OctetString):
|
|
2054
|
-
return obj.value.to_str()
|
|
2055
|
-
else:
|
|
2056
|
-
return str(obj.value)
|
|
2057
|
-
else:
|
|
2058
|
-
return 'недоступен'
|
|
2059
|
-
|
|
2060
|
-
@deprecated("<use ReadObjAttr>")
|
|
2061
|
-
async def read_attribute(self, obj: ic.COSEMInterfaceClasses | str,
|
|
2062
|
-
attr_index: int):
|
|
2063
|
-
# TODO: redundant, use read_attr?
|
|
2064
|
-
if isinstance(obj, str):
|
|
2065
|
-
obj = self.objects.get_object(obj)
|
|
2066
|
-
self.get_get_request_normal(obj.get_attr_descriptor(
|
|
2067
|
-
value=attr_index,
|
|
2068
|
-
with_selection=bool(self.negotiated_conformance.selective_access)))
|
|
2069
|
-
start_read_time: float = time.perf_counter()
|
|
2070
|
-
data = (await self.read_data_block()).unwrap()
|
|
2071
|
-
self.last_transfer_time = datetime.timedelta(seconds=time.perf_counter()-start_read_time)
|
|
2072
|
-
obj.set_attr(attr_index, data)
|
|
2073
|
-
|
|
2074
|
-
@deprecated("use execute_method2")
|
|
2075
|
-
async def execute_method(self, meth_desc: ut.CosemMethodDescriptor) -> result.Ok | result.Error:
|
|
2076
|
-
data = self.get_action_request_normal_old(meth_desc)
|
|
2077
|
-
return await self.read_data_block()
|
|
2078
|
-
|
|
2079
|
-
async def execute_method2(self, obj: ic.COSEMInterfaceClasses, i: int, mip=None) -> result.Ok | result.Error:
|
|
2080
|
-
data = self.get_action_request_normal(
|
|
2081
|
-
meth_desc=obj.get_meth_descriptor(i),
|
|
2082
|
-
method=obj.get_meth_element(i).DATA_TYPE() if mip is None else mip)
|
|
2083
|
-
return await self.read_data_block()
|
|
2084
|
-
|
|
2085
|
-
async def is_equal_attribute(self, obj: ic.COSEMInterfaceClasses, attr_index: int | str, with_time: bool | datetime.datetime = False) -> bool:
|
|
2086
|
-
self.get_get_request_normal(obj.get_attr_descriptor(attr_index))
|
|
2087
|
-
data = (await self.read_data_block()).unwrap()
|
|
2088
|
-
if obj.get_attr(attr_index).encoding == data:
|
|
2089
|
-
return True
|
|
2090
|
-
else:
|
|
2091
|
-
return False
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing_extensions import deprecated
|
|
3
|
+
import dataclasses
|
|
4
|
+
import time
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from functools import cached_property, reduce
|
|
7
|
+
from struct import pack
|
|
8
|
+
from collections import deque
|
|
9
|
+
from itertools import count
|
|
10
|
+
from enum import IntEnum, auto, IntFlag
|
|
11
|
+
from typing import TextIO, Deque, Any, Callable, Optional
|
|
12
|
+
import threading
|
|
13
|
+
import datetime
|
|
14
|
+
import os
|
|
15
|
+
import hashlib
|
|
16
|
+
from Cryptodome.Cipher import AES
|
|
17
|
+
from StructResult import result
|
|
18
|
+
from DLMS_SPODES_communications import Network, Serial, RS485, BLEKPZ, base
|
|
19
|
+
from DLMS_SPODES.cosem_interface_classes import overview
|
|
20
|
+
from DLMS_SPODES.cosem_interface_classes.collection import Collection, InterfaceClass, ic, cdt, ut, Data, AssociationLN
|
|
21
|
+
from DLMS_SPODES.cosem_interface_classes.security_setup.ver1 import SecuritySuite
|
|
22
|
+
from DLMS_SPODES.enums import (
|
|
23
|
+
Transmit, Application, ActionRequest, ReadResponse, ServiceError, AssociationResult, SetRequest, ConfirmedServiceError, AARQapdu, ACSEAPDU, XDLMSAPDU,
|
|
24
|
+
VariableAccessSpecification, AcseServiceUser
|
|
25
|
+
)
|
|
26
|
+
from DLMS_SPODES.cosem_interface_classes.association_ln import mechanism_id, method
|
|
27
|
+
from DLMS_SPODES.cosem_interface_classes.association_ln.authentication_mechanism_name import AuthenticationMechanismName
|
|
28
|
+
from DLMS_SPODES.hdlc import frame, sub_layer
|
|
29
|
+
from DLMS_SPODES import pdu_enums as pdu, exceptions as exc
|
|
30
|
+
from DLMS_SPODES.types.implementations import enums, long_unsigneds, bitstrings, octet_string
|
|
31
|
+
from DLMSCommunicationProfile import communication_profile as c_pf, OSI
|
|
32
|
+
from .gurux_dlms import GXDLMSSettings, GXByteBuffer, GXReplyData, GXDLMSException
|
|
33
|
+
from .gurux_dlms.enums import Security, Standard, BerType, RequestTypes, Service
|
|
34
|
+
from .gurux_dlms.GXDLMS import GXDLMS
|
|
35
|
+
from .gurux_dlms.GXDLMSLNParameters import GXDLMSLNParameters
|
|
36
|
+
from .gurux_dlms.GXDLMSSNParameters import GXDLMSSNParameters
|
|
37
|
+
from .gurux_dlms.AesGcmParameter import AesGcmParameter
|
|
38
|
+
from .gurux_dlms.GXCiphering import GXCiphering
|
|
39
|
+
from .gurux_dlms.GXDLMSConfirmedServiceError import GXDLMSConfirmedServiceError
|
|
40
|
+
from .gurux_dlms.GXDLMSChippering import GXDLMSChippering
|
|
41
|
+
from .gurux_dlms import CountType
|
|
42
|
+
from .gurux_dlms.internal._GXCommon import _GXCommon
|
|
43
|
+
from .logger import logger, LogLevel as logL
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def copy_with_align(data: bytes, block_size: int = 16) -> bytes:
|
|
47
|
+
""" fill by zeros to full 16 bytes blocks """
|
|
48
|
+
return data + bytes((block_size - len(data) % block_size) % block_size)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
TZ = datetime.timezone(datetime.datetime.now() - datetime.datetime.utcnow())
|
|
52
|
+
""" os time zone """
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_os_datetime() -> datetime.datetime:
|
|
56
|
+
""" return os datetime with time zone """
|
|
57
|
+
return datetime.datetime.now(TZ)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_os_time() -> str:
|
|
61
|
+
""" return os time with time zone """
|
|
62
|
+
return get_os_datetime().strftime('%H:%M:%S')
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class State(ABC):
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def __str__(self):
|
|
69
|
+
""""""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclasses.dataclass
|
|
73
|
+
class Text(State):
|
|
74
|
+
value: str
|
|
75
|
+
|
|
76
|
+
def __str__(self):
|
|
77
|
+
return self.value
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class IDFactory:
|
|
81
|
+
def __init__(self, prefix: str):
|
|
82
|
+
self.count = count()
|
|
83
|
+
self.value = set()
|
|
84
|
+
self.prefix = prefix
|
|
85
|
+
|
|
86
|
+
def create(self) -> str:
|
|
87
|
+
id_ = F"{self.prefix}{next(self.count)}"
|
|
88
|
+
"""for identification before LDN reading"""
|
|
89
|
+
while True:
|
|
90
|
+
if id_ not in self.value:
|
|
91
|
+
self.register(id_)
|
|
92
|
+
return id_
|
|
93
|
+
else:
|
|
94
|
+
id_ = F"{self.prefix}{next(self.count)}"
|
|
95
|
+
|
|
96
|
+
def register(self, id_: str):
|
|
97
|
+
if id_ not in self.value:
|
|
98
|
+
self.value.add(id_)
|
|
99
|
+
else:
|
|
100
|
+
raise ValueError(F"error in register ID={id_}: already exist")
|
|
101
|
+
|
|
102
|
+
def remove(self, value: str) -> bool:
|
|
103
|
+
try:
|
|
104
|
+
self.value.remove(value)
|
|
105
|
+
return True
|
|
106
|
+
except KeyError:
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class Client:
|
|
111
|
+
id: str | None
|
|
112
|
+
name: str = "unknown"
|
|
113
|
+
com_profile: c_pf.CommunicationProfile
|
|
114
|
+
__del_cb: Callable[[str], bool] | None
|
|
115
|
+
__universal: bool
|
|
116
|
+
level: OSI
|
|
117
|
+
log_file: TextIO
|
|
118
|
+
media: base.Media | None
|
|
119
|
+
lock: asyncio.Lock
|
|
120
|
+
last_transfer_time: datetime.timedelta | None
|
|
121
|
+
connection_time_release: int
|
|
122
|
+
received_frames: Deque[frame.Frame]
|
|
123
|
+
current_obj: InterfaceClass | None
|
|
124
|
+
reply: GXReplyData
|
|
125
|
+
settings: GXDLMSSettings
|
|
126
|
+
__sap: enums.ClientSAP
|
|
127
|
+
secret: bytes
|
|
128
|
+
SA: frame.Address
|
|
129
|
+
DA: frame.Address
|
|
130
|
+
negotiated_conformance: bitstrings.Conformance
|
|
131
|
+
_objects: Optional[Collection]
|
|
132
|
+
APP_CONTEXT_NAME = cdt.OctetString("60857405080101")
|
|
133
|
+
"""AssociationLN.application_context_name a-xdr encode"""
|
|
134
|
+
DEF_DLMS_VER: int = 6
|
|
135
|
+
"""DLMS version by default"""
|
|
136
|
+
m_id: mechanism_id.MechanismIdElement
|
|
137
|
+
"""None is the AUTO from current association"""
|
|
138
|
+
addr_size: frame.AddressLength
|
|
139
|
+
logging_disable: bool
|
|
140
|
+
state: State
|
|
141
|
+
|
|
142
|
+
def __init__(self,
|
|
143
|
+
SAP: int = 0x10,
|
|
144
|
+
secret: str | bytes = "",
|
|
145
|
+
conformance: str = None,
|
|
146
|
+
addr_size: int = -1,
|
|
147
|
+
media: base.Media = None,
|
|
148
|
+
id_: str | int = None,
|
|
149
|
+
m_id: int = 0,
|
|
150
|
+
universal: bool = False,
|
|
151
|
+
del_cb: Callable[[str], bool] = None,
|
|
152
|
+
com_profile: c_pf.CommunicationProfile = None):
|
|
153
|
+
self.com_profile = c_pf.HDLC() if com_profile is None else com_profile
|
|
154
|
+
"""communication profile"""
|
|
155
|
+
self.id = id_
|
|
156
|
+
"""for identification before LDN reading"""
|
|
157
|
+
self.__universal = universal
|
|
158
|
+
"""matching LDN if True else change server Type"""
|
|
159
|
+
self.__del_cb = del_cb
|
|
160
|
+
"""callback to unregister id"""
|
|
161
|
+
self.logging_disable = False
|
|
162
|
+
"""turn off logging by default"""
|
|
163
|
+
self._objects = None
|
|
164
|
+
self.__sap = enums.ClientSAP(SAP)
|
|
165
|
+
"""Service Access Point. Default <Public>"""
|
|
166
|
+
self.media = Serial(port="COM3") if media is None else media
|
|
167
|
+
""" physical layer """
|
|
168
|
+
if com_profile is None:
|
|
169
|
+
self.com_profile = c_pf.HDLC()
|
|
170
|
+
self.server_SAP = long_unsigneds.ServerSAP(1)
|
|
171
|
+
if isinstance(secret, str):
|
|
172
|
+
self.secret = bytes.fromhex(secret)
|
|
173
|
+
elif isinstance(secret, bytes):
|
|
174
|
+
self.secret = secret
|
|
175
|
+
self.protocol_version = cdt.BitString('1') # max 8 bit
|
|
176
|
+
""" Protocol Version of the AARQ APDU """
|
|
177
|
+
# TODO: REMOVE IT BULLSHIT
|
|
178
|
+
self.invocationCounter = '0.0.43.1.0.255'
|
|
179
|
+
self.lock = asyncio.Lock()
|
|
180
|
+
""" lock for exchange access to device """
|
|
181
|
+
self.addr_size = frame.AddressLength(addr_size)
|
|
182
|
+
"""server address size, -1 is AUTO"""
|
|
183
|
+
self.m_id = mechanism_id.MechanismIdElement(m_id)
|
|
184
|
+
# from AssociationLN.xDLMSinfo
|
|
185
|
+
self.quality_of_service = 0
|
|
186
|
+
self.receive_pdu_size = 0xffff # max available
|
|
187
|
+
self.proposed_conformance = bitstrings.Conformance(conformance)
|
|
188
|
+
self.negotiated_conformance = self.proposed_conformance.copy()
|
|
189
|
+
|
|
190
|
+
self.last_transfer_time = None
|
|
191
|
+
""" decided time transfer from server to client """
|
|
192
|
+
|
|
193
|
+
self.connection_time_release = 10
|
|
194
|
+
""" number of second for port release after inactivity """
|
|
195
|
+
|
|
196
|
+
self.received_frames = deque()
|
|
197
|
+
""" HDLC frames container from server """
|
|
198
|
+
|
|
199
|
+
self.send_frames = deque()
|
|
200
|
+
self.level = OSI.NONE
|
|
201
|
+
"""OSI level"""
|
|
202
|
+
self.settings = GXDLMSSettings(False)
|
|
203
|
+
|
|
204
|
+
self.current_obj = None
|
|
205
|
+
""" current transferring object. For progress bar now """
|
|
206
|
+
|
|
207
|
+
# from Gurux Client
|
|
208
|
+
self.use_protected_release = False
|
|
209
|
+
""" Gurux Client: If protected release is used release is including a ciphered xDLMS Initiate request. """
|
|
210
|
+
|
|
211
|
+
self.state = Text("undefined")
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def objects(self) -> Collection:
|
|
215
|
+
if self._objects is None:
|
|
216
|
+
raise exc.DLMSException("client hasn't objects")
|
|
217
|
+
return self._objects
|
|
218
|
+
|
|
219
|
+
def __del__(self):
|
|
220
|
+
if self.__del_cb:
|
|
221
|
+
self.__del_cb(self.id)
|
|
222
|
+
|
|
223
|
+
def is_universal(self) -> bool:
|
|
224
|
+
return self.__universal
|
|
225
|
+
|
|
226
|
+
def log(self, level: logL, msg: str | State):
|
|
227
|
+
"""use logger with level and extra=LDN"""
|
|
228
|
+
if not self.logging_disable:
|
|
229
|
+
logger.log(level=level,
|
|
230
|
+
msg=str(msg),
|
|
231
|
+
extra={"id": self._objects.LDN.value.to_str() if (self._objects and self._objects.LDN.value) else F"{self.id}"})
|
|
232
|
+
if level == logL.STATE and isinstance(msg, State):
|
|
233
|
+
self.state = msg
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def SAP(self) -> enums.ClientSAP:
|
|
237
|
+
return self.__sap
|
|
238
|
+
|
|
239
|
+
@SAP.setter
|
|
240
|
+
def SAP(self, value):
|
|
241
|
+
"""change SAP if associationLN possible"""
|
|
242
|
+
new_SAP = enums.ClientSAP(value)
|
|
243
|
+
if self._objects is not None:
|
|
244
|
+
self._objects.sap2association(new_SAP)
|
|
245
|
+
else:
|
|
246
|
+
"""OK"""
|
|
247
|
+
self.__sap.set(value)
|
|
248
|
+
|
|
249
|
+
def get_ass_id(self) -> int:
|
|
250
|
+
"""return current Association ID"""
|
|
251
|
+
return int(self.current_association.logical_name.e)
|
|
252
|
+
|
|
253
|
+
def get_channel_index(self) -> int:
|
|
254
|
+
"""todo: remove in future. get communication channel by media"""
|
|
255
|
+
match self.media:
|
|
256
|
+
case Serial(): return 0
|
|
257
|
+
case RS485(): return 1
|
|
258
|
+
case Network(): return 2
|
|
259
|
+
case BLEKPZ(): return 3
|
|
260
|
+
case _: raise ValueError(F"can't calculate channel index by media: {self.media}")
|
|
261
|
+
|
|
262
|
+
def get_frame(self, read_data: bytearray, reply: GXReplyData) -> frame.Frame | None:
|
|
263
|
+
reply.complete = False
|
|
264
|
+
while len(read_data) != 0:
|
|
265
|
+
new_frame = frame.Frame.try_from(read_data)
|
|
266
|
+
if not isinstance(new_frame, frame.Frame):
|
|
267
|
+
return None
|
|
268
|
+
reply.complete = True
|
|
269
|
+
if new_frame.is_for_me(self.DA, self.SA):
|
|
270
|
+
self.received_frames.append(new_frame)
|
|
271
|
+
if new_frame.is_segmentation:
|
|
272
|
+
reply.moreData |= RequestTypes.FRAME
|
|
273
|
+
else:
|
|
274
|
+
reply.moreData &= ~RequestTypes.FRAME
|
|
275
|
+
# check control TODO: rewrite it
|
|
276
|
+
if new_frame.control.is_unnumbered():
|
|
277
|
+
if new_frame.control in (frame.Control.UA_F, frame.Control.SNRM_P):
|
|
278
|
+
self.settings.resetFrameSequence()
|
|
279
|
+
return new_frame
|
|
280
|
+
elif new_frame.control == frame.Control.UI_PF:
|
|
281
|
+
self.log(logL.WARN, """ TODO: Here Notify handler """)
|
|
282
|
+
else:
|
|
283
|
+
self.log(logL.INFO, F'Can\'t processing HDLC Frame: {new_frame.control}')
|
|
284
|
+
elif new_frame.control.is_supervisory():
|
|
285
|
+
self.settings.receiverFrame = frame.Control.next_receiver_sequence(self.settings.receiverFrame)
|
|
286
|
+
return new_frame
|
|
287
|
+
elif self.settings.senderFrame.is_info():
|
|
288
|
+
expected = frame.Control.next_receiver_sequence(frame.Control.next_send_sequence(self.settings.receiverFrame))
|
|
289
|
+
if new_frame.control == expected:
|
|
290
|
+
self.settings.receiverFrame = new_frame.control
|
|
291
|
+
return new_frame
|
|
292
|
+
else:
|
|
293
|
+
self.log(logL.INFO, F'Invalid HDLC Frame: {new_frame.control} Expected: {expected}')
|
|
294
|
+
else:
|
|
295
|
+
expected = frame.Control.next_send_sequence(self.settings.receiverFrame)
|
|
296
|
+
# If answer for RR.
|
|
297
|
+
if new_frame.control == expected:
|
|
298
|
+
self.settings.receiverFrame = new_frame.control
|
|
299
|
+
return new_frame
|
|
300
|
+
else:
|
|
301
|
+
self.log(logL.INFO, F'Invalid HDLC Frame: {new_frame.control} Expected: {expected}')
|
|
302
|
+
self.log(logL.WARN, F"Drop frame {new_frame}")
|
|
303
|
+
else:
|
|
304
|
+
self.log(logL.WARN, F"ALIEN frame {new_frame}, expect with SA:{self.SA}")
|
|
305
|
+
# FROM GURUX - if new_frame.control == frame.Control.UI_PF: # search next frame in read_data
|
|
306
|
+
|
|
307
|
+
def handleGbt(self, reply: GXReplyData) -> result.Ok | result.Error:
|
|
308
|
+
index = reply.data.position - 1
|
|
309
|
+
reply.windowSize = self.settings.windowSize
|
|
310
|
+
bc = reply.data.getUInt8()
|
|
311
|
+
reply.streaming = (bc & 0x40) != 0
|
|
312
|
+
windowSize = int(bc & 0x3F)
|
|
313
|
+
bn = reply.data.getUInt16()
|
|
314
|
+
bna = reply.data.getUInt16()
|
|
315
|
+
reply.blockNumber = bn
|
|
316
|
+
reply.blockNumberAck = bna
|
|
317
|
+
self.settings.blockNumberAck = reply.blockNumber
|
|
318
|
+
reply.command = None
|
|
319
|
+
len_ = _GXCommon.getObjectCount(reply.data)
|
|
320
|
+
if len_ > reply.data.size - reply.data.position:
|
|
321
|
+
reply.complete = False
|
|
322
|
+
return result.Error.from_e(RuntimeError("not enouth reply data size"))
|
|
323
|
+
GXDLMS.getDataFromBlock(reply.data, index)
|
|
324
|
+
if (bc & 0x80) == 0:
|
|
325
|
+
reply.moreData = (RequestTypes(reply.moreData | RequestTypes.GBT))
|
|
326
|
+
else:
|
|
327
|
+
reply.moreData = (RequestTypes(reply.moreData & ~RequestTypes.GBT))
|
|
328
|
+
if reply.data.size != 0:
|
|
329
|
+
reply.data.position = 0
|
|
330
|
+
if isinstance(res_pdu := self.getPdu(), result.Error):
|
|
331
|
+
return res_pdu.with_msg("handle GBT")
|
|
332
|
+
# if reply.data.position != reply.data.size and (reply.command == XDLMSAPDU.READ_RESPONSE or reply.command == XDLMSAPDU.GET_RESPONSE) and (reply.moreData == RequestTypes.NONE or reply.peek):
|
|
333
|
+
# reply.data.position = 0
|
|
334
|
+
# cls.getValueFromData(settings, reply)
|
|
335
|
+
return result.OK
|
|
336
|
+
|
|
337
|
+
def getPdu(self, reply: GXReplyData) -> result.Ok | result.Error:
|
|
338
|
+
# TODO: make return pdu
|
|
339
|
+
if reply.command is None:
|
|
340
|
+
if reply.data.size - reply.data.position == 0:
|
|
341
|
+
return result.Error(ValueError("Invalid PDU"), "getpdu")
|
|
342
|
+
index = reply.data.position
|
|
343
|
+
reply.command = XDLMSAPDU(reply.data.getUInt8())
|
|
344
|
+
match reply.command:
|
|
345
|
+
case XDLMSAPDU.GET_RESPONSE:
|
|
346
|
+
response_type: int = reply.data.getUInt8()
|
|
347
|
+
invoke_id_and_priority = reply.data.getUInt8() # TODO: matching with setting params
|
|
348
|
+
match response_type:
|
|
349
|
+
case pdu.GetResponse.NORMAL:
|
|
350
|
+
match reply.data.getUInt8(): # Get-Data-Result[0]
|
|
351
|
+
case 0:
|
|
352
|
+
GXDLMS.getDataFromBlock(reply.data, 0)
|
|
353
|
+
case 1:
|
|
354
|
+
reply.error = pdu.DataAccessResult(reply.data.getUInt8())
|
|
355
|
+
if reply.error != 0:
|
|
356
|
+
return result.Error.from_e(exc.ResultError(reply.error), "get pdu")
|
|
357
|
+
case err:
|
|
358
|
+
return result.Error.from_e(ValueError(F'Got Get-Data-Result[0] {err}, expect 0 or 1'), "get pdu")
|
|
359
|
+
GXDLMS.getDataFromBlock(reply.data, 0)
|
|
360
|
+
case pdu.GetResponse.WITH_DATABLOCK:
|
|
361
|
+
last_block = reply.data.getUInt8()
|
|
362
|
+
if last_block == 0:
|
|
363
|
+
reply.moreData |= RequestTypes.DATABLOCK
|
|
364
|
+
else:
|
|
365
|
+
reply.moreData &= ~RequestTypes.DATABLOCK
|
|
366
|
+
block_number = reply.data.getUInt32()
|
|
367
|
+
if block_number == 0 and self.settings.blockIndex == 1: # if start block_index == 0
|
|
368
|
+
self.settings.setBlockIndex(0)
|
|
369
|
+
if block_number != self.settings.blockIndex:
|
|
370
|
+
return result.Error.from_e(ValueError(F"Invalid Block number. It is {block_number} and it should be {self.settings.blockIndex}."), "get pdu")
|
|
371
|
+
match reply.data.getUInt8(): # DataBlock-G.result,
|
|
372
|
+
case 0:
|
|
373
|
+
if reply.data.position != len(reply.data):
|
|
374
|
+
block_length = _GXCommon.getObjectCount(reply.data)
|
|
375
|
+
if (reply.moreData & RequestTypes.FRAME) == 0:
|
|
376
|
+
if block_length > len(reply.data) - reply.data.position:
|
|
377
|
+
return result.Error.from_e(ValueError("Invalid block length."), "get pdu")
|
|
378
|
+
reply.command = None
|
|
379
|
+
if block_length == 0:
|
|
380
|
+
reply.data.size = index
|
|
381
|
+
else:
|
|
382
|
+
GXDLMS.getDataFromBlock(reply.data, index)
|
|
383
|
+
if reply.moreData == RequestTypes.NONE:
|
|
384
|
+
if not reply.peek:
|
|
385
|
+
reply.data.position = 0
|
|
386
|
+
self.settings.resetBlockIndex()
|
|
387
|
+
if reply.moreData == RequestTypes.NONE and self.settings and self.settings.command == XDLMSAPDU.GET_REQUEST \
|
|
388
|
+
and self.settings.commandType == pdu.GetResponse.WITH_LIST:
|
|
389
|
+
GXDLMS.handleGetResponseWithList(self.settings, reply)
|
|
390
|
+
return result.OK
|
|
391
|
+
case 1:
|
|
392
|
+
reply.error = pdu.DataAccessResult(reply.data.getUInt8())
|
|
393
|
+
if reply.error != 0:
|
|
394
|
+
return result.Error.from_e(exc.ResultError(reply.error), "get pdu")
|
|
395
|
+
case err:
|
|
396
|
+
return result.Error.from_e(ValueError(F'Got DataBlock-G.result {err}, expect 0 or 1'), "get pdu")
|
|
397
|
+
case pdu.GetResponse.WITH_LIST:
|
|
398
|
+
GXDLMS.handleGetResponseWithList(self.settings, reply)
|
|
399
|
+
return result.OK
|
|
400
|
+
case err:
|
|
401
|
+
return result.Error.from_e(ValueError(F"Got Invalid Get response {err}, expect {', '.join(map(lambda it: F'{it.name} = {it.value}', pdu.GetResponse))}"), "get pdu")
|
|
402
|
+
case XDLMSAPDU.READ_RESPONSE:
|
|
403
|
+
if not GXDLMS.handleReadResponse(self.settings, reply, index):
|
|
404
|
+
return result.OK
|
|
405
|
+
case XDLMSAPDU.SET_RESPONSE:
|
|
406
|
+
response_type: int = reply.data.getUInt8()
|
|
407
|
+
invoke_id_and_priority = reply.data.getUInt8() # TODO: matching with setting params
|
|
408
|
+
match response_type:
|
|
409
|
+
case pdu.SetResponse.NORMAL:
|
|
410
|
+
reply.error = pdu.DataAccessResult(reply.data.getUInt8())
|
|
411
|
+
if reply.error != 0:
|
|
412
|
+
return result.Error.from_e(exc.ResultError(reply.error), "get pdu")
|
|
413
|
+
case pdu.SetResponse.DATABLOCK:
|
|
414
|
+
block_number = reply.data.getUInt32()
|
|
415
|
+
case pdu.SetResponse.LAST_DATABLOCK:
|
|
416
|
+
reply.error = pdu.DataAccessResult(reply.data.getUInt8())
|
|
417
|
+
if reply.error != 0:
|
|
418
|
+
return result.Error.from_e(exc.ResultError(reply.error), "get pdu")
|
|
419
|
+
block_number = reply.data.getUInt32()
|
|
420
|
+
case pdu.SetResponse.LAST_DATABLOCK_WITH_LIST:
|
|
421
|
+
raise RuntimeError("Not released in Client")
|
|
422
|
+
case pdu.SetResponse.WITH_LIST:
|
|
423
|
+
cnt = _GXCommon.getObjectCount(reply.data)
|
|
424
|
+
pos = 0
|
|
425
|
+
while pos != cnt:
|
|
426
|
+
reply.error = pdu.DataAccessResult(reply.data.getUInt8())
|
|
427
|
+
if reply.error != 0:
|
|
428
|
+
return result.Error.from_e(exc.ResultError(reply.error), "get pdu")
|
|
429
|
+
pos += 1
|
|
430
|
+
case err:
|
|
431
|
+
return result.Error.from_e(ValueError(F"Got Invalid Set response {err}, expect {', '.join(map(lambda it: F'{it.name} = {it.value}', pdu.SetResponse))}"), "get pdu")
|
|
432
|
+
case XDLMSAPDU.WRITE_RESPONSE:
|
|
433
|
+
cnt = _GXCommon.getObjectCount(reply.data)
|
|
434
|
+
pos = 0
|
|
435
|
+
while pos != cnt:
|
|
436
|
+
ret = reply.data.getUInt8()
|
|
437
|
+
if ret != 0:
|
|
438
|
+
reply.error = reply.data.getUInt8()
|
|
439
|
+
pos += 1
|
|
440
|
+
case XDLMSAPDU.ACTION_RESPONSE:
|
|
441
|
+
action_response = reply.data.getUInt8()
|
|
442
|
+
invoke_id_and_priority = reply.data.getUInt8()
|
|
443
|
+
match action_response:
|
|
444
|
+
case pdu.ActionResponse.NORMAL:
|
|
445
|
+
reply.error = pdu.ActionResult(reply.data.getUInt8())
|
|
446
|
+
if reply.error != 0:
|
|
447
|
+
return result.Error(exc.ResultError(reply.error), "get pdu")
|
|
448
|
+
if reply.data.position < reply.data.size:
|
|
449
|
+
ret = reply.data.getUInt8()
|
|
450
|
+
if ret == 0:
|
|
451
|
+
GXDLMS.getDataFromBlock(reply.data, 0)
|
|
452
|
+
elif ret == 1:
|
|
453
|
+
ret = int(reply.data.getUInt8())
|
|
454
|
+
if ret != 0:
|
|
455
|
+
reply.error = reply.data.getUInt8()
|
|
456
|
+
if ret == 9 and reply.error == 16:
|
|
457
|
+
reply.data.position = reply.data.position - 2
|
|
458
|
+
GXDLMS.getDataFromBlock(reply.data, 0)
|
|
459
|
+
reply.error = 0
|
|
460
|
+
ret = 0
|
|
461
|
+
else:
|
|
462
|
+
GXDLMS.getDataFromBlock(reply.data, 0)
|
|
463
|
+
else:
|
|
464
|
+
return result.Error.from_e(Exception("HandleActionResponseNormal failed. " + "Invalid tag."), "get pdu")
|
|
465
|
+
case pdu.ActionResponse.WITH_PBLOCK:
|
|
466
|
+
raise RuntimeError("Not released in Client")
|
|
467
|
+
case pdu.ActionResponse.WITH_LIST:
|
|
468
|
+
raise RuntimeError("Not released in Client")
|
|
469
|
+
case pdu.ActionResponse.NEXT_PBLOCK:
|
|
470
|
+
raise RuntimeError("Not released in Client")
|
|
471
|
+
case err:
|
|
472
|
+
return result.Error.from_e(ValueError(F"got {pdu.ActionResponse}: {err}, expect {', '.join(map(lambda it: F'{it.name} = {it.value}', pdu.ActionResponse))}"), "get pdu")
|
|
473
|
+
case XDLMSAPDU.ACCESS_RESPONSE:
|
|
474
|
+
data = reply.data
|
|
475
|
+
invokeId = reply.data.getUInt32()
|
|
476
|
+
len_ = reply.data.getUInt8()
|
|
477
|
+
tmp = None
|
|
478
|
+
if len_ != 0:
|
|
479
|
+
tmp = bytearray(len_)
|
|
480
|
+
data.get(tmp)
|
|
481
|
+
reply.time = _GXCommon.changeType(self.settings, tmp, DataType.DATETIME)
|
|
482
|
+
data.getUInt8()
|
|
483
|
+
case XDLMSAPDU.GENERAL_BLOCK_TRANSFER:
|
|
484
|
+
if not self.settings.isServer and (reply.moreData & RequestTypes.FRAME) == 0:
|
|
485
|
+
if isinstance(res_gbt := self.handleGbt(reply), result.Error):
|
|
486
|
+
return res_gbt
|
|
487
|
+
case ACSEAPDU.AARQ | ACSEAPDU.AARE:
|
|
488
|
+
# This is parsed later.
|
|
489
|
+
reply.data.position = reply.data.position - 1
|
|
490
|
+
case ACSEAPDU.RLRE | ACSEAPDU.RLRQ:
|
|
491
|
+
pass
|
|
492
|
+
case XDLMSAPDU.CONFIRMED_SERVICE_ERROR:
|
|
493
|
+
GXDLMS.handleConfirmedServiceError(reply)
|
|
494
|
+
case XDLMSAPDU.EXCEPTION_RESPONSE:
|
|
495
|
+
GXDLMS.handleExceptionResponse(reply)
|
|
496
|
+
case XDLMSAPDU.GET_REQUEST | XDLMSAPDU.READ_REQUEST | XDLMSAPDU.WRITE_REQUEST | XDLMSAPDU.SET_REQUEST | XDLMSAPDU.ACTION_REQUEST:
|
|
497
|
+
pass
|
|
498
|
+
case XDLMSAPDU.GLO_READ_REQUEST | XDLMSAPDU.GLO_WRITE_REQUEST | XDLMSAPDU.GLO_GET_REQUEST | XDLMSAPDU.GLO_SET_REQUEST | XDLMSAPDU.GLO_ACTION_REQUEST | \
|
|
499
|
+
XDLMSAPDU.DED_GET_REQUEST | XDLMSAPDU.DED_SET_REQUEST | XDLMSAPDU.DED_ACTION_REQUEST:
|
|
500
|
+
if self.settings.cipher is None:
|
|
501
|
+
return result.Error.from_e(ServiceError("Secure connection is not supported."), "get pdu")
|
|
502
|
+
if (reply.moreData & RequestTypes.FRAME) == 0:
|
|
503
|
+
reply.data.position = reply.data.position - 1
|
|
504
|
+
p = None
|
|
505
|
+
if self.settings.cipher.dedicatedKey and (OSI.APPLICATION in self.level):
|
|
506
|
+
p = AesGcmParameter(self.settings.sourceSystemTitle, self.settings.cipher.dedicatedKey, self.settings.cipher.authenticationKey)
|
|
507
|
+
else:
|
|
508
|
+
p = AesGcmParameter(self.settings.sourceSystemTitle, self.settings.cipher.blockCipherKey, self.settings.cipher.authenticationKey)
|
|
509
|
+
tmp = GXCiphering.decrypt(self.settings.cipher, p, reply.data)
|
|
510
|
+
reply.data.clear()
|
|
511
|
+
reply.data.set(tmp)
|
|
512
|
+
reply.command = XDLMSAPDU(reply.data.getUInt8())
|
|
513
|
+
if reply.command == XDLMSAPDU.DATA_NOTIFICATION or reply.command == XDLMSAPDU.INFORMATION_REPORT_REQUEST:
|
|
514
|
+
reply.command = None
|
|
515
|
+
reply.data.position = reply.data.position - 1
|
|
516
|
+
if isinstance(res_pdu := self.getPdu(reply), result.Error):
|
|
517
|
+
return res_pdu
|
|
518
|
+
else:
|
|
519
|
+
reply.data.position = reply.data.position - 1
|
|
520
|
+
case XDLMSAPDU.GLO_READ_RESPONSE | XDLMSAPDU.GLO_WRITE_RESPONSE | XDLMSAPDU.GLO_GET_RESPONSE | XDLMSAPDU.GLO_SET_RESPONSE | XDLMSAPDU.GLO_ACTION_RESPONSE | \
|
|
521
|
+
XDLMSAPDU.GENERAL_GLO_CIPHERING | XDLMSAPDU.GLO_EVENT_NOTIFICATION_REQUEST | XDLMSAPDU.DED_GET_RESPONSE | XDLMSAPDU.DED_SET_RESPONSE | \
|
|
522
|
+
XDLMSAPDU.DED_ACTION_RESPONSE | XDLMSAPDU.GENERAL_DED_CIPHERING | XDLMSAPDU.DED_EVENT_NOTIFICATION_REQUEST:
|
|
523
|
+
if self.settings.cipher is None:
|
|
524
|
+
return result.Error.from_e(ServiceError("Secure connection is not supported."), "get pdu")
|
|
525
|
+
if (reply.moreData & RequestTypes.FRAME) == 0:
|
|
526
|
+
reply.data.position = reply.data.position - 1
|
|
527
|
+
bb = GXByteBuffer(reply.data)
|
|
528
|
+
reply.data.size = reply.data.position = index
|
|
529
|
+
p = None
|
|
530
|
+
if self.settings.cipher.dedicatedKey and (OSI.APPLICATION in self.level):
|
|
531
|
+
p = AesGcmParameter(0, self.settings.sourceSystemTitle, self.settings.cipher.dedicatedKey, self.settings.cipher.authenticationKey)
|
|
532
|
+
else:
|
|
533
|
+
p = AesGcmParameter(0, self.settings.sourceSystemTitle, self.settings.cipher.blockCipherKey, self.settings.cipher.authenticationKey)
|
|
534
|
+
reply.data.set(GXCiphering.decrypt(self.settings.cipher, p, bb))
|
|
535
|
+
reply.command = None
|
|
536
|
+
if isinstance(res_pdu := self.getPdu(reply), result.Error):
|
|
537
|
+
return res_pdu
|
|
538
|
+
reply.cipherIndex = reply.data.size
|
|
539
|
+
case XDLMSAPDU.DATA_NOTIFICATION:
|
|
540
|
+
GXDLMS.handleDataNotification(self.settings, reply)
|
|
541
|
+
data = reply.data
|
|
542
|
+
start = data.position - 1
|
|
543
|
+
invokeId = data.getUInt32()
|
|
544
|
+
reply.time = None
|
|
545
|
+
len_ = data.getUInt8()
|
|
546
|
+
tmp = None
|
|
547
|
+
if len_ != 0:
|
|
548
|
+
tmp = bytearray(len_)
|
|
549
|
+
data.get(tmp)
|
|
550
|
+
dt = DataType.DATETIME
|
|
551
|
+
if len_ == 4:
|
|
552
|
+
dt = DataType.TIME
|
|
553
|
+
elif len_ == 5:
|
|
554
|
+
dt = DataType.DATE
|
|
555
|
+
info = _GXDataInfo()
|
|
556
|
+
info.type_ = dt
|
|
557
|
+
reply.time = _GXCommon.getData(self.settings, GXByteBuffer(tmp), info)
|
|
558
|
+
GXDLMS.getDataFromBlock(reply.data, start)
|
|
559
|
+
GXDLMS.getValueFromData(self.settings, reply)
|
|
560
|
+
case XDLMSAPDU.EVENT_NOTIFICATION_REQUEST:
|
|
561
|
+
pass
|
|
562
|
+
case XDLMSAPDU.INFORMATION_REPORT_REQUEST:
|
|
563
|
+
pass
|
|
564
|
+
case XDLMSAPDU.GENERAL_CIPHERING:
|
|
565
|
+
if self.settings.cipher is None:
|
|
566
|
+
return result.Error.from_e(ServiceError("Secure connection is not supported."), "get pdu")
|
|
567
|
+
if (reply.moreData & RequestTypes.FRAME) == 0:
|
|
568
|
+
reply.data.position = reply.data.position - 1
|
|
569
|
+
p = AesGcmParameter(0, self.settings.sourceSystemTitle, self.settings.cipher.blockCipherKey, self.settings.cipher.authenticationKey)
|
|
570
|
+
tmp = GXCiphering.decrypt(self.settings.cipher, p, reply.data)
|
|
571
|
+
reply.data.clear()
|
|
572
|
+
reply.data.set(tmp)
|
|
573
|
+
reply.command = None
|
|
574
|
+
if p.security:
|
|
575
|
+
if isinstance(res_pdu := self.getPdu(reply), result.Error):
|
|
576
|
+
return res_pdu
|
|
577
|
+
case XDLMSAPDU.GATEWAY_REQUEST:
|
|
578
|
+
pass
|
|
579
|
+
case XDLMSAPDU.GATEWAY_RESPONSE:
|
|
580
|
+
reply.data.getUInt8()
|
|
581
|
+
len_ = _GXCommon.getObjectCount(reply.data)
|
|
582
|
+
pda = bytearray(len_)
|
|
583
|
+
reply.data.get(pda)
|
|
584
|
+
GXDLMS.getDataFromBlock(reply, index)
|
|
585
|
+
reply.command = None
|
|
586
|
+
if isinstance(res_pdu := self.getPdu(reply), result.Error):
|
|
587
|
+
return res_pdu
|
|
588
|
+
case _:
|
|
589
|
+
return result.Error.from_e(ValueError("Invalid PDU Command."), "get pdu")
|
|
590
|
+
elif (reply.moreData & RequestTypes.FRAME) == 0:
|
|
591
|
+
if not reply.peek and reply.moreData == RequestTypes.NONE:
|
|
592
|
+
if reply.command == ACSEAPDU.AARE or reply.command == ACSEAPDU.AARQ:
|
|
593
|
+
reply.data.position = 0
|
|
594
|
+
else:
|
|
595
|
+
reply.data.position = 1
|
|
596
|
+
if reply.command == XDLMSAPDU.GENERAL_BLOCK_TRANSFER:
|
|
597
|
+
reply.data.position = reply.cipherIndex + 1
|
|
598
|
+
if isinstance(res_gbt := self.handleGbt(reply), result.Error):
|
|
599
|
+
return res_gbt
|
|
600
|
+
reply.cipherIndex = reply.data.size
|
|
601
|
+
reply.command = None
|
|
602
|
+
elif self.settings.isServer:
|
|
603
|
+
if reply.command in (
|
|
604
|
+
XDLMSAPDU.GLO_READ_REQUEST, XDLMSAPDU.GLO_WRITE_REQUEST, XDLMSAPDU.GLO_GET_REQUEST, XDLMSAPDU.GLO_SET_REQUEST, XDLMSAPDU.GLO_ACTION_REQUEST,
|
|
605
|
+
XDLMSAPDU.GLO_EVENT_NOTIFICATION_REQUEST, XDLMSAPDU.DED_GET_REQUEST, XDLMSAPDU.DED_SET_REQUEST, XDLMSAPDU.DED_ACTION_REQUEST,
|
|
606
|
+
XDLMSAPDU.DED_EVENT_NOTIFICATION_REQUEST):
|
|
607
|
+
reply.command = None
|
|
608
|
+
reply.data.position = reply.getCipherIndex()
|
|
609
|
+
if isinstance(res_pdu := self.getPdu(reply), result.Error):
|
|
610
|
+
return res_pdu
|
|
611
|
+
else:
|
|
612
|
+
reply.command = None
|
|
613
|
+
if reply.command in (
|
|
614
|
+
XDLMSAPDU.GLO_READ_RESPONSE,
|
|
615
|
+
XDLMSAPDU.GLO_WRITE_RESPONSE,
|
|
616
|
+
XDLMSAPDU.GLO_GET_RESPONSE,
|
|
617
|
+
XDLMSAPDU.GLO_SET_RESPONSE,
|
|
618
|
+
XDLMSAPDU.GLO_ACTION_RESPONSE,
|
|
619
|
+
XDLMSAPDU.DED_GET_RESPONSE,
|
|
620
|
+
XDLMSAPDU.DED_SET_RESPONSE,
|
|
621
|
+
XDLMSAPDU.DED_ACTION_RESPONSE,
|
|
622
|
+
XDLMSAPDU.GENERAL_GLO_CIPHERING,
|
|
623
|
+
XDLMSAPDU.GENERAL_DED_CIPHERING
|
|
624
|
+
):
|
|
625
|
+
reply.data.position = reply.cipherIndex
|
|
626
|
+
if isinstance(res_pdu := self.getPdu(reply), result.Error):
|
|
627
|
+
return res_pdu
|
|
628
|
+
if (
|
|
629
|
+
reply.command == XDLMSAPDU.READ_RESPONSE
|
|
630
|
+
and reply.totalCount > 1
|
|
631
|
+
):
|
|
632
|
+
if not GXDLMS.handleReadResponse(self.settings, reply, 0):
|
|
633
|
+
return result.OK
|
|
634
|
+
|
|
635
|
+
if (
|
|
636
|
+
reply.command == XDLMSAPDU.READ_RESPONSE
|
|
637
|
+
and reply.commandType == ReadResponse.DATA_BLOCK_RESULT
|
|
638
|
+
and (reply.moreData & RequestTypes.FRAME) != 0
|
|
639
|
+
):
|
|
640
|
+
return result.OK
|
|
641
|
+
if (
|
|
642
|
+
reply.data.position != reply.data.size
|
|
643
|
+
and (
|
|
644
|
+
reply.moreData == RequestTypes.NONE
|
|
645
|
+
or reply.peek)
|
|
646
|
+
and reply.command in (
|
|
647
|
+
XDLMSAPDU.READ_RESPONSE,
|
|
648
|
+
XDLMSAPDU.GET_RESPONSE,
|
|
649
|
+
XDLMSAPDU.ACTION_RESPONSE,
|
|
650
|
+
XDLMSAPDU.DATA_NOTIFICATION)
|
|
651
|
+
):
|
|
652
|
+
return result.OK
|
|
653
|
+
# GXDLMS.getValueFromData(self.settings, reply)
|
|
654
|
+
|
|
655
|
+
def __is_frame(self, notify, read_data: bytearray, reply_: GXReplyData) -> bool:
|
|
656
|
+
reply = GXByteBuffer(read_data)
|
|
657
|
+
is_notify: bool = False
|
|
658
|
+
match self.com_profile:
|
|
659
|
+
case c_pf.HDLC():
|
|
660
|
+
recv_frame = self.get_frame(read_data, reply_)
|
|
661
|
+
if recv_frame is not None:
|
|
662
|
+
self.log(logL.INFO, F"RX: {recv_frame.content.hex(' ')}")
|
|
663
|
+
if recv_frame.control == frame.Control.UI_PF:
|
|
664
|
+
target = notify # use instead of reply_ in getPdu(target). see in Gurux to do
|
|
665
|
+
is_notify = True
|
|
666
|
+
reply_.frameId = recv_frame.control
|
|
667
|
+
else: # TODO: GURUX redundant
|
|
668
|
+
# self.write_trace(F"RX {self.id}: {get_os_time()} {read_data}", TraceLevel.ERROR)
|
|
669
|
+
reply_.frameId = frame.Control(0)
|
|
670
|
+
case c_pf.TCPUDPIP(): # getTcpData TODO: check it
|
|
671
|
+
target = reply_
|
|
672
|
+
if len(reply) - reply.position < 8:
|
|
673
|
+
target.complete = False
|
|
674
|
+
return True
|
|
675
|
+
pos = reply.position
|
|
676
|
+
while reply.position < len(reply) - 1:
|
|
677
|
+
value = reply.getUInt16()
|
|
678
|
+
if value == 1:
|
|
679
|
+
# checkWrapperAddress
|
|
680
|
+
if self.settings.isServer:
|
|
681
|
+
value = reply.getUInt16()
|
|
682
|
+
if self.settings.clientAddress != 0 and self.settings.clientAddress != value:
|
|
683
|
+
raise Exception("Source addresses do not match. It is " + str(value) + ". It should be " + str(self.settings.clientAddress) + ".")
|
|
684
|
+
self.settings.clientAddress = value
|
|
685
|
+
value = reply.getUInt16()
|
|
686
|
+
if self.settings.serverAddress != 0 and self.settings.serverAddress != value:
|
|
687
|
+
raise Exception("Destination addresses do not match. It is " + str(value) + ". It should be " + str(self.settings.serverAddress) + ".")
|
|
688
|
+
self.settings.serverAddress = value
|
|
689
|
+
else:
|
|
690
|
+
value = reply.getUInt16()
|
|
691
|
+
if self.settings.clientAddress != 0 and self.settings.serverAddress != value:
|
|
692
|
+
if notify is None:
|
|
693
|
+
raise Exception("Source addresses do not match. It is " + str(value) + ". It should be " + str(self.settings.serverAddress) + ".")
|
|
694
|
+
notify.serverAddress = value
|
|
695
|
+
target = notify
|
|
696
|
+
else:
|
|
697
|
+
self.settings.serverAddress = value
|
|
698
|
+
value = reply.getUInt16()
|
|
699
|
+
if self.settings.clientAddress != 0 and self.settings.clientAddress != value:
|
|
700
|
+
if notify is None:
|
|
701
|
+
raise Exception("Destination addresses do not match. It is " + str(value) + ". It should be " + str(self.settings.clientAddress) + ".")
|
|
702
|
+
target = notify
|
|
703
|
+
notify.clientAddress = value
|
|
704
|
+
else:
|
|
705
|
+
self.settings.clientAddress = value
|
|
706
|
+
#
|
|
707
|
+
value = reply.getUInt16()
|
|
708
|
+
complete = not (len(reply) - reply.position) < value
|
|
709
|
+
if complete and (len(reply) - reply.position) != value:
|
|
710
|
+
self.log(logL.DEB, "Data length is " + str(value) + "and there are " + str(len(reply) - reply.position) + " bytes.")
|
|
711
|
+
target.complete = complete
|
|
712
|
+
if not complete:
|
|
713
|
+
reply.position = pos
|
|
714
|
+
else:
|
|
715
|
+
target.packetLength = (reply.position + value)
|
|
716
|
+
break
|
|
717
|
+
else:
|
|
718
|
+
reply.position = reply.position - 1
|
|
719
|
+
if target is not reply_:
|
|
720
|
+
is_notify = True
|
|
721
|
+
case c_pf.MBUS(): # not realised see how
|
|
722
|
+
GXDLMS.getMBusData(self.settings, reply, reply_)
|
|
723
|
+
case _: raise ValueError("Invalid Interface type.")
|
|
724
|
+
if not reply_.complete:
|
|
725
|
+
return False
|
|
726
|
+
|
|
727
|
+
# TODO: relocate notify to read_data_type
|
|
728
|
+
if notify and not is_notify:
|
|
729
|
+
#Check command to make sure it's not notify message.
|
|
730
|
+
if reply_.command in (XDLMSAPDU.DATA_NOTIFICATION,
|
|
731
|
+
XDLMSAPDU.GLO_EVENT_NOTIFICATION_REQUEST,
|
|
732
|
+
XDLMSAPDU.INFORMATION_REPORT_REQUEST,
|
|
733
|
+
XDLMSAPDU.EVENT_NOTIFICATION_REQUEST,
|
|
734
|
+
XDLMSAPDU.DED_INFORMATION_REPORT_REQUEST,
|
|
735
|
+
XDLMSAPDU.DED_EVENT_NOTIFICATION_REQUEST):
|
|
736
|
+
is_notify = True
|
|
737
|
+
notify.complete = reply_.complete
|
|
738
|
+
notify.command = reply_.command
|
|
739
|
+
reply_.command = None
|
|
740
|
+
reply_.time = None
|
|
741
|
+
notify.reply_.set(reply_.data)
|
|
742
|
+
# notify.value = reply_.value
|
|
743
|
+
reply_.data.trim()
|
|
744
|
+
if is_notify:
|
|
745
|
+
return False
|
|
746
|
+
return True
|
|
747
|
+
|
|
748
|
+
async def read_data_block(self) -> result.SimpleOrError[bytes]: # todo: make depend from CommunicationProfile
|
|
749
|
+
self.received_frames.clear()
|
|
750
|
+
reply = GXReplyData()
|
|
751
|
+
while self.send_frames:
|
|
752
|
+
send_frame = self.send_frames.popleft()
|
|
753
|
+
notify = GXReplyData()
|
|
754
|
+
reply.error = 0
|
|
755
|
+
recv_buf: bytearray = bytearray()
|
|
756
|
+
if not reply.isStreaming():
|
|
757
|
+
await self.media.send(send_frame.content)
|
|
758
|
+
self.log(logL.INFO, F"TX: {send_frame.content.hex(" ")}")
|
|
759
|
+
attempt: int = 1
|
|
760
|
+
while attempt < 3:
|
|
761
|
+
if not await self.media.receive(recv_buf): # todo: make for BLE
|
|
762
|
+
self.log(logL.WARN, F'Data receive failed: Try to resend {attempt + 1}/3. RX_buffer: {recv_buf.hex(" ")}')
|
|
763
|
+
await self.media.send(send_frame.content)
|
|
764
|
+
attempt += 1
|
|
765
|
+
continue
|
|
766
|
+
if self.__is_frame(notify, recv_buf, reply):
|
|
767
|
+
break
|
|
768
|
+
if notify.data.size != 0:
|
|
769
|
+
if not notify.isMoreData():
|
|
770
|
+
notify.clear()
|
|
771
|
+
continue
|
|
772
|
+
else:
|
|
773
|
+
"""our frame not was found"""
|
|
774
|
+
else:
|
|
775
|
+
return result.Error.from_e(TimeoutError("Failed to receive reply from the device in given time"), "read data block")
|
|
776
|
+
recv_buf.clear()
|
|
777
|
+
match reply.error:
|
|
778
|
+
case 0:
|
|
779
|
+
"""errors is absence"""
|
|
780
|
+
case 4:
|
|
781
|
+
return result.Error.from_e(exc.NoObject(), "read data block")
|
|
782
|
+
case _:
|
|
783
|
+
return result.Error.from_e(GXDLMSException(reply.error), "read data block")
|
|
784
|
+
if self.received_frames[-1].control.is_info() or self.received_frames[-1].control == frame.Control.UI_PF:
|
|
785
|
+
if self.received_frames[-1].is_segmentation:
|
|
786
|
+
"""pass handle frame. wait all information"""
|
|
787
|
+
else:
|
|
788
|
+
llc = sub_layer.LLC(frame.Frame.join_info(self.received_frames))
|
|
789
|
+
|
|
790
|
+
reply.data.position = len(reply.data)
|
|
791
|
+
reply.data.set(llc.info)
|
|
792
|
+
if isinstance(res_pdu := self.getPdu(reply), result.Error):
|
|
793
|
+
return res_pdu
|
|
794
|
+
# TODO: LLC to PDU
|
|
795
|
+
else:
|
|
796
|
+
received_frame = self.received_frames.popleft()
|
|
797
|
+
if send_frame.control == frame.Control.SNRM_P:
|
|
798
|
+
self.com_profile.negotiation.set_from_UA(received_frame.info)
|
|
799
|
+
self.log(logL.INFO, F"negotiation setup: {self.com_profile.negotiation}")
|
|
800
|
+
if reply.isMoreData():
|
|
801
|
+
if reply.isStreaming():
|
|
802
|
+
data = None
|
|
803
|
+
else:
|
|
804
|
+
# Generates an acknowledgment message, with which the server is informed to send next packets. Frame type. Acknowledgment message as byte array
|
|
805
|
+
if reply.moreData == RequestTypes.NONE:
|
|
806
|
+
return result.Error.from_e(ValueError("Invalid receiverReady RequestTypes parameter."), msg="read data block")
|
|
807
|
+
# Get next frame.
|
|
808
|
+
if (reply.moreData & RequestTypes.FRAME) != 0:
|
|
809
|
+
id_ = self.settings.getReceiverReady()
|
|
810
|
+
# return GXDLMS.getHdlcFrame(settings, id_, None)
|
|
811
|
+
self.add_frames_to_queue(frame.Control(id_))
|
|
812
|
+
else:
|
|
813
|
+
if self.settings.getUseLogicalNameReferencing():
|
|
814
|
+
if self.settings.isServer:
|
|
815
|
+
cmd = XDLMSAPDU.GET_RESPONSE
|
|
816
|
+
else:
|
|
817
|
+
cmd = XDLMSAPDU.GET_REQUEST
|
|
818
|
+
else:
|
|
819
|
+
if self.settings.isServer:
|
|
820
|
+
cmd = XDLMSAPDU.READ_RESPONSE
|
|
821
|
+
else:
|
|
822
|
+
cmd = XDLMSAPDU.READ_REQUEST
|
|
823
|
+
if reply.moreData == RequestTypes.GBT:
|
|
824
|
+
p = GXDLMSLNParameters(self.settings, 0, XDLMSAPDU.GENERAL_BLOCK_TRANSFER, 0, None, None, 0xff)
|
|
825
|
+
p.WindowSize = reply.windowSize
|
|
826
|
+
p.blockNumberAck = reply.blockNumberAck
|
|
827
|
+
p.blockIndex = reply.blockNumber
|
|
828
|
+
p.Streaming = False
|
|
829
|
+
messages = self.getLnMessages(p) # TODO: test it
|
|
830
|
+
else:
|
|
831
|
+
# Get next block.
|
|
832
|
+
bb = GXByteBuffer(4)
|
|
833
|
+
if self.settings.getUseLogicalNameReferencing():
|
|
834
|
+
bb.setUInt32(self.settings.blockIndex)
|
|
835
|
+
else:
|
|
836
|
+
bb.setUInt16(self.settings.blockIndex)
|
|
837
|
+
self.settings.increaseBlockIndex()
|
|
838
|
+
if self.settings.getUseLogicalNameReferencing():
|
|
839
|
+
p = GXDLMSLNParameters(self.settings, 0, cmd, pdu.GetResponse.WITH_DATABLOCK, bb, None, 0xff)
|
|
840
|
+
messages = self.getLnMessages(p)
|
|
841
|
+
else:
|
|
842
|
+
p = GXDLMSSNParameters(self.settings, cmd, 1, VariableAccessSpecification.BLOCK_NUMBER_ACCESS, bb, None)
|
|
843
|
+
messages = self.getSnMessages(p)
|
|
844
|
+
data = messages
|
|
845
|
+
return result.Simple(reply.data.get_data())
|
|
846
|
+
|
|
847
|
+
def getSnMessages(self, p: GXDLMSSNParameters):
|
|
848
|
+
reply = GXByteBuffer()
|
|
849
|
+
messages = list()
|
|
850
|
+
frame_ = 0x0
|
|
851
|
+
if p.command == XDLMSAPDU.INFORMATION_REPORT_REQUEST or p.command == XDLMSAPDU.DATA_NOTIFICATION:
|
|
852
|
+
frame_ = 0x13
|
|
853
|
+
while True:
|
|
854
|
+
ciphering = p.settings.cipher and p.settings.cipher.security != Security.NONE and p.command != ACSEAPDU.AARQ and p.command != ACSEAPDU.AARE
|
|
855
|
+
if (
|
|
856
|
+
not ciphering
|
|
857
|
+
and isinstance(self.com_profile, c_pf.HDLC)
|
|
858
|
+
):
|
|
859
|
+
if p.settings.isServer:
|
|
860
|
+
reply.set(_GXCommon.LLC_REPLY_BYTES)
|
|
861
|
+
elif not reply:
|
|
862
|
+
reply.set(_GXCommon.LLC_SEND_BYTES)
|
|
863
|
+
cnt = 0
|
|
864
|
+
cipherSize = 0
|
|
865
|
+
if ciphering:
|
|
866
|
+
cipherSize = GXDLMS._CIPHERING_HEADER_SIZE
|
|
867
|
+
if p.data:
|
|
868
|
+
cnt = p.data.size - p.data.position
|
|
869
|
+
if p.command == XDLMSAPDU.INFORMATION_REPORT_REQUEST:
|
|
870
|
+
reply.setUInt8(p.command)
|
|
871
|
+
if not p.time:
|
|
872
|
+
reply.setUInt8(cdt.NullData.TAG)
|
|
873
|
+
else:
|
|
874
|
+
pos = len(reply)
|
|
875
|
+
_GXCommon.setData(p.settings, reply, cdt.OctetString.TAG, p.time)
|
|
876
|
+
reply.move(pos + 1, pos, len(reply) - pos - 1)
|
|
877
|
+
_GXCommon.setObjectCount(p.count, reply)
|
|
878
|
+
reply.set(p.attributeDescriptor)
|
|
879
|
+
elif p.command != ACSEAPDU.AARQ and p.command != ACSEAPDU.AARE:
|
|
880
|
+
reply.setUInt8(p.command)
|
|
881
|
+
if p.count != 0xFF:
|
|
882
|
+
_GXCommon.setObjectCount(p.count, reply)
|
|
883
|
+
if p.requestType != 0xFF:
|
|
884
|
+
reply.setUInt8(p.requestType)
|
|
885
|
+
reply.set(p.attributeDescriptor)
|
|
886
|
+
if not p.settings.is_multiple_block():
|
|
887
|
+
p.multipleBlocks = len(reply) + cipherSize + cnt > p.settings.maxPduSize
|
|
888
|
+
if p.settings.is_multiple_block():
|
|
889
|
+
reply.size = 0
|
|
890
|
+
if (
|
|
891
|
+
not ciphering
|
|
892
|
+
and isinstance(self.com_profile, c_pf.HDLC)
|
|
893
|
+
):
|
|
894
|
+
if p.settings.isServer:
|
|
895
|
+
reply.set(_GXCommon.LLC_REPLY_BYTES)
|
|
896
|
+
elif not reply:
|
|
897
|
+
reply.set(_GXCommon.LLC_SEND_BYTES)
|
|
898
|
+
match p.command:
|
|
899
|
+
case XDLMSAPDU.WRITE_REQUEST:
|
|
900
|
+
p.requestType = VariableAccessSpecification.WRITE_DATA_BLOCK_ACCESS
|
|
901
|
+
case XDLMSAPDU.READ_REQUEST:
|
|
902
|
+
p.requestType = VariableAccessSpecification.READ_DATA_BLOCK_ACCESS
|
|
903
|
+
case XDLMSAPDU.READ_RESPONSE:
|
|
904
|
+
p.requestType = ReadResponse.DATA_BLOCK_RESULT
|
|
905
|
+
case _:
|
|
906
|
+
raise ValueError("Invalid command.")
|
|
907
|
+
reply.setUInt8(p.command)
|
|
908
|
+
reply.setUInt8(1)
|
|
909
|
+
if p.requestType != 0xFF:
|
|
910
|
+
reply.setUInt8(p.requestType)
|
|
911
|
+
cnt = GXDLMS.appendMultipleSNBlocks(p, reply)
|
|
912
|
+
else:
|
|
913
|
+
cnt = GXDLMS.appendMultipleSNBlocks(p, reply)
|
|
914
|
+
if p.data:
|
|
915
|
+
reply.set(p.data, p.data.position, cnt)
|
|
916
|
+
if p.data and p.data.position == p.data.size:
|
|
917
|
+
p.settings.index = 0
|
|
918
|
+
p.settings.count = 0
|
|
919
|
+
if ciphering and p.command != ACSEAPDU.AARQ and p.command != ACSEAPDU.AARE:
|
|
920
|
+
cipher = p.settings.cipher
|
|
921
|
+
s = AesGcmParameter(self.getGloMessage(p.command), cipher.systemTitle, cipher.blockCipherKey, cipher.authenticationKey)
|
|
922
|
+
s.security = cipher.security
|
|
923
|
+
s.invocationCounter = cipher.invocationCounter
|
|
924
|
+
tmp = GXCiphering.encrypt(s, reply.array())
|
|
925
|
+
assert not tmp
|
|
926
|
+
reply.size = 0
|
|
927
|
+
if isinstance(self.com_profile, c_pf.HDLC):
|
|
928
|
+
if p.settings.isServer:
|
|
929
|
+
reply.set(_GXCommon.LLC_REPLY_BYTES)
|
|
930
|
+
elif not reply:
|
|
931
|
+
reply.set(_GXCommon.LLC_SEND_BYTES)
|
|
932
|
+
reply.set(tmp)
|
|
933
|
+
if p.command != ACSEAPDU.AARQ and p.command != ACSEAPDU.AARE:
|
|
934
|
+
assert not p.settings.maxPduSize < len(reply)
|
|
935
|
+
while reply.position != len(reply):
|
|
936
|
+
match self.com_profile:
|
|
937
|
+
case c_pf.TCPUDPIP():
|
|
938
|
+
messages.append(GXDLMS.getWrapperFrame(p.settings, p.command, reply))
|
|
939
|
+
case c_pf.HDLC():
|
|
940
|
+
messages.append(GXDLMS.getHdlcFrame(p.settings, frame_, reply))
|
|
941
|
+
if reply.position != len(reply):
|
|
942
|
+
frame_ = p.settings.getNextSend(False)
|
|
943
|
+
case _:
|
|
944
|
+
raise ValueError("InterfaceType")
|
|
945
|
+
reply.clear()
|
|
946
|
+
frame_ = 0
|
|
947
|
+
if not p.data or p.data.position == p.data.size:
|
|
948
|
+
break
|
|
949
|
+
return messages
|
|
950
|
+
|
|
951
|
+
def receiverReady(self, reply):
|
|
952
|
+
""" Generates an acknowledgment message, with which the server is informed to send next packets. Frame type. Acknowledgment message as byte array. """
|
|
953
|
+
if reply.moreData == RequestTypes.NONE:
|
|
954
|
+
raise ValueError("Invalid receiverReady RequestTypes parameter.")
|
|
955
|
+
# Get next frame.
|
|
956
|
+
if (reply.moreData & RequestTypes.FRAME) != 0:
|
|
957
|
+
id_ = self.settings.getReceiverReady()
|
|
958
|
+
# return GXDLMS.getHdlcFrame(settings, id_, None)
|
|
959
|
+
return self.add_frames_to_queue(frame.Control(id_))
|
|
960
|
+
if self.settings.getUseLogicalNameReferencing():
|
|
961
|
+
if self.settings.isServer:
|
|
962
|
+
cmd = XDLMSAPDU.GET_RESPONSE
|
|
963
|
+
else:
|
|
964
|
+
cmd = XDLMSAPDU.GET_REQUEST
|
|
965
|
+
else:
|
|
966
|
+
if self.settings.isServer:
|
|
967
|
+
cmd = XDLMSAPDU.READ_RESPONSE
|
|
968
|
+
else:
|
|
969
|
+
cmd = XDLMSAPDU.READ_REQUEST
|
|
970
|
+
|
|
971
|
+
if reply.moreData == RequestTypes.GBT:
|
|
972
|
+
p = GXDLMSLNParameters(self.settings, 0, XDLMSAPDU.GENERAL_BLOCK_TRANSFER, 0, None, None, 0xff)
|
|
973
|
+
p.WindowSize = reply.windowSize
|
|
974
|
+
p.blockNumberAck = reply.blockNumberAck
|
|
975
|
+
p.blockIndex = reply.blockNumber
|
|
976
|
+
p.Streaming = False
|
|
977
|
+
reply = self.getLnMessages(p) # TODO: test it
|
|
978
|
+
else:
|
|
979
|
+
# Get next block.
|
|
980
|
+
bb = GXByteBuffer(4)
|
|
981
|
+
if self.settings.getUseLogicalNameReferencing():
|
|
982
|
+
bb.setUInt32(self.settings.blockIndex)
|
|
983
|
+
else:
|
|
984
|
+
bb.setUInt16(self.settings.blockIndex)
|
|
985
|
+
self.settings.increaseBlockIndex()
|
|
986
|
+
if self.settings.getUseLogicalNameReferencing():
|
|
987
|
+
p = GXDLMSLNParameters(self.settings, 0, cmd, pdu.GetResponse.WITH_DATABLOCK, bb, None, 0xff)
|
|
988
|
+
reply = self.getLnMessages(p)
|
|
989
|
+
else:
|
|
990
|
+
p = GXDLMSSNParameters(self.settings, cmd, 1, VariableAccessSpecification.BLOCK_NUMBER_ACCESS, bb, None)
|
|
991
|
+
reply = self.getSnMessages(p)
|
|
992
|
+
return reply
|
|
993
|
+
|
|
994
|
+
def set_params(self, field: str, value: str):
|
|
995
|
+
self.__dict__[field] = eval(value)
|
|
996
|
+
|
|
997
|
+
async def close(self) -> result.StrictOk | result.Error:
|
|
998
|
+
"""close , media is open"""
|
|
999
|
+
res = result.StrictOk()
|
|
1000
|
+
self.log(logL.DEB, "close")
|
|
1001
|
+
if self.level > OSI.DATA_LINK:
|
|
1002
|
+
# Release is call only for secured connections. All meters are not supporting Release and it's causing problems.
|
|
1003
|
+
if (
|
|
1004
|
+
isinstance(self.com_profile, c_pf.TCPUDPIP)
|
|
1005
|
+
or (
|
|
1006
|
+
isinstance(self.com_profile, c_pf.HDLC)
|
|
1007
|
+
and self.settings.cipher.security != Security.NONE
|
|
1008
|
+
)
|
|
1009
|
+
):
|
|
1010
|
+
self.releaseRequest()
|
|
1011
|
+
if isinstance(res_rdb := await self.read_data_block(), result.Error):
|
|
1012
|
+
res.append_err(res_rdb.err)
|
|
1013
|
+
self.log(logL.WARN, "don't support release ReleaseRequest")
|
|
1014
|
+
self.level = OSI.DATA_LINK
|
|
1015
|
+
# hdlc close
|
|
1016
|
+
if isinstance(res_diconnect_req := await self.disconnect_request(), result.Error):
|
|
1017
|
+
res.append_err(res_diconnect_req.err)
|
|
1018
|
+
self.level -= OSI.DATA_LINK
|
|
1019
|
+
return res
|
|
1020
|
+
|
|
1021
|
+
async def disconnect_request(self) -> result.Ok | result.Error:
|
|
1022
|
+
""" Sent to server DISC """
|
|
1023
|
+
if isinstance(self.com_profile, c_pf.HDLC):
|
|
1024
|
+
self.add_frames_to_queue(frame.Control.DISC_P)
|
|
1025
|
+
else:
|
|
1026
|
+
self.releaseRequest()
|
|
1027
|
+
return await self.read_data_block()
|
|
1028
|
+
|
|
1029
|
+
@cached_property
|
|
1030
|
+
def n_phases(self) -> int:
|
|
1031
|
+
"""cached phases amount"""
|
|
1032
|
+
return self.objects.get_n_phases()
|
|
1033
|
+
|
|
1034
|
+
async def encode(self,
|
|
1035
|
+
obj: ic.COSEMInterfaceClasses,
|
|
1036
|
+
index: int,
|
|
1037
|
+
value: str | int) -> cdt.CommonDataType:
|
|
1038
|
+
"""encode attribute value from string if possible, else return None(for CHOICE variant) during connection"""
|
|
1039
|
+
if (ret := obj.encode(index, value)) is not None:
|
|
1040
|
+
return ret
|
|
1041
|
+
else:
|
|
1042
|
+
await self.read_attribute(obj, index)
|
|
1043
|
+
ret = obj.get_attr(index).copy()
|
|
1044
|
+
ret.set(value)
|
|
1045
|
+
return ret
|
|
1046
|
+
|
|
1047
|
+
# TODO: remove in future
|
|
1048
|
+
def parseApplicationAssociationResponse(self, data: bytes):
|
|
1049
|
+
""" Parse server's challenge if HLS authentication is used. Received reply from the server. todo: refactoring here """
|
|
1050
|
+
ic = 0
|
|
1051
|
+
value = cdt.OctetString(data)
|
|
1052
|
+
match self.m_id:
|
|
1053
|
+
case mechanism_id.HIGH_GMAC:
|
|
1054
|
+
secret = self.settings.sourceSystemTitle
|
|
1055
|
+
bb = GXByteBuffer(value)
|
|
1056
|
+
bb.getUInt8()
|
|
1057
|
+
ic = bb.getUInt32()
|
|
1058
|
+
case mechanism_id.HIGH_SHA256:
|
|
1059
|
+
tmp2 = GXByteBuffer()
|
|
1060
|
+
tmp2.set(self.secret)
|
|
1061
|
+
tmp2.set(self.settings.sourceSystemTitle)
|
|
1062
|
+
tmp2.set(self.settings.cipher.systemTitle)
|
|
1063
|
+
tmp2.set(self.settings.ctoSChallenge)
|
|
1064
|
+
tmp2.set(self.settings.stoCChallenge)
|
|
1065
|
+
secret = tmp2.array()
|
|
1066
|
+
case mechanism_id.HIGH: secret = self.secret
|
|
1067
|
+
case mechanism_id.HIGH_ECDSA: raise ValueError("ECDSA is not supported.")
|
|
1068
|
+
case _ as mech_id: raise ValueError(F'{mech_id} is not supported')
|
|
1069
|
+
tmp = self.secure(ic, self.settings.ctoSChallenge, bytes(secret))
|
|
1070
|
+
challenge = cdt.OctetString(bytearray(tmp))
|
|
1071
|
+
equals = challenge == value
|
|
1072
|
+
if not equals:
|
|
1073
|
+
self.log(logL.DEB, "Invalid StoC:" + GXByteBuffer.hex(value, True) + "-" + GXByteBuffer.hex(tmp, True))
|
|
1074
|
+
if not equals:
|
|
1075
|
+
raise Exception("parseApplicationAssociationResponse failed. " + " Server to Client do not match.")
|
|
1076
|
+
self.level |= OSI.APPLICATION
|
|
1077
|
+
|
|
1078
|
+
def secure(self, ic, data, secret: bytes) -> bytes:
|
|
1079
|
+
""" TODO: """
|
|
1080
|
+
if not isinstance(secret, bytes):
|
|
1081
|
+
raise ValueError(F'cipher is not bytes type, got {secret.__class__}')
|
|
1082
|
+
# Get server Challenge.
|
|
1083
|
+
challenge = GXByteBuffer()
|
|
1084
|
+
# Get shared secret
|
|
1085
|
+
match self.m_id:
|
|
1086
|
+
case mechanism_id.HIGH:
|
|
1087
|
+
if len(secret) != 16:
|
|
1088
|
+
raise ValueError(F'length secret must be 16, got {len(secret)}')
|
|
1089
|
+
cipher = AES.new(secret, AES.MODE_ECB)
|
|
1090
|
+
ciphertext: bytes = cipher.encrypt(copy_with_align(data))
|
|
1091
|
+
return ciphertext
|
|
1092
|
+
case mechanism_id.HIGH_GMAC:
|
|
1093
|
+
challenge.set(data)
|
|
1094
|
+
d = challenge.array()
|
|
1095
|
+
# SC is always Security.Authentication.
|
|
1096
|
+
p = AesGcmParameter(0, secret, self.settings.cipher.blockCipherKey, self.settings.cipher.authenticationKey)
|
|
1097
|
+
p.security = Security.AUTHENTICATION
|
|
1098
|
+
p.invocationCounter = ic
|
|
1099
|
+
p.type_ = CountType.TAG
|
|
1100
|
+
challenge.clear()
|
|
1101
|
+
challenge.setUInt8(Security.AUTHENTICATION)
|
|
1102
|
+
challenge.setUInt32(p.invocationCounter)
|
|
1103
|
+
challenge.set(GXDLMSChippering.encryptAesGcm(p, d))
|
|
1104
|
+
return challenge.array()
|
|
1105
|
+
case mechanism_id.HIGH_SHA256:
|
|
1106
|
+
challenge.set(secret)
|
|
1107
|
+
d = challenge.array()
|
|
1108
|
+
md = hashlib.sha256()
|
|
1109
|
+
md.update(d)
|
|
1110
|
+
return md.digest()
|
|
1111
|
+
case mechanism_id.HIGH_MD5:
|
|
1112
|
+
challenge.set(data)
|
|
1113
|
+
challenge.set(secret)
|
|
1114
|
+
d = challenge.array()
|
|
1115
|
+
md = hashlib.md5()
|
|
1116
|
+
md.update(d)
|
|
1117
|
+
return md.digest()
|
|
1118
|
+
case mechanism_id.HIGH_SHA1:
|
|
1119
|
+
challenge.set(data)
|
|
1120
|
+
challenge.set(secret)
|
|
1121
|
+
d = challenge.array()
|
|
1122
|
+
md = hashlib.sha1()
|
|
1123
|
+
md.update(d)
|
|
1124
|
+
return md.digest()
|
|
1125
|
+
case mechanism_id.HIGH_ECDSA: raise Exception("ECDSA is not supported.")
|
|
1126
|
+
case _ as err: raise Exception(F'Not support {err}')
|
|
1127
|
+
|
|
1128
|
+
def getApplicationAssociationRequest(self):
|
|
1129
|
+
""" Get challenge request if HLS authentication is used. """
|
|
1130
|
+
match self.m_id, self.secret:
|
|
1131
|
+
case mechanism_id.HIGH_ECDSA | mechanism_id.HIGH_GMAC, None: raise ValueError('Password is invalid.')
|
|
1132
|
+
case _: pass
|
|
1133
|
+
self.settings.resetBlockIndex()
|
|
1134
|
+
match self.m_id:
|
|
1135
|
+
case mechanism_id.HIGH_GMAC: pw = self.settings.cipher.systemTitle
|
|
1136
|
+
case mechanism_id.HIGH_SHA256:
|
|
1137
|
+
tmp = GXByteBuffer()
|
|
1138
|
+
tmp.set(self.secret)
|
|
1139
|
+
tmp.set(self.settings.cipher.systemTitle)
|
|
1140
|
+
tmp.set(self.settings.sourceSystemTitle)
|
|
1141
|
+
tmp.set(self.settings.stoCChallenge)
|
|
1142
|
+
tmp.set(self.settings.ctoSChallenge)
|
|
1143
|
+
pw = tmp.array()
|
|
1144
|
+
case _: pw = self.secret
|
|
1145
|
+
ic = 0
|
|
1146
|
+
if self.settings.cipher:
|
|
1147
|
+
ic = self.settings.cipher.invocationCounter
|
|
1148
|
+
challenge = self.secure(ic, self.settings.getStoCChallenge(), pw)
|
|
1149
|
+
if self.settings.getUseLogicalNameReferencing():
|
|
1150
|
+
return self.get_action_request_normal(
|
|
1151
|
+
meth_desc=ut.CosemMethodDescriptor((overview.ClassID.ASSOCIATION_LN, ut.CosemObjectInstanceId(F"0.0.40.0.0.255"), ut.CosemObjectMethodId(1))),
|
|
1152
|
+
# meth_desc=self.current_association.get_meth_descriptor(1),
|
|
1153
|
+
method=method.ReplyToHLSAuthentication(bytearray(challenge)))
|
|
1154
|
+
else:
|
|
1155
|
+
return self.method2(0xFA00, 12, 8, challenge, cdt.OctetString.TAG) # TODO: rewrite old client.method
|
|
1156
|
+
|
|
1157
|
+
def parseAARE(self, pdu: bytes) -> AcseServiceUser:
|
|
1158
|
+
# Get AARE tag and length
|
|
1159
|
+
buff = GXByteBuffer(pdu)
|
|
1160
|
+
tag = buff.getUInt8()
|
|
1161
|
+
if self.settings.isServer:
|
|
1162
|
+
if tag != (BerType.APPLICATION | BerType.CONSTRUCTED | AARQapdu.PROTOCOL_VERSION):
|
|
1163
|
+
raise ValueError("Invalid tag.")
|
|
1164
|
+
else:
|
|
1165
|
+
if tag != (BerType.APPLICATION | BerType.CONSTRUCTED | AARQapdu.APPLICATION_CONTEXT_NAME):
|
|
1166
|
+
raise ValueError("Invalid tag.")
|
|
1167
|
+
if _GXCommon.getObjectCount(buff) > len(buff) - buff.position:
|
|
1168
|
+
raise ValueError("PDU: Not enough data.")
|
|
1169
|
+
resultComponent = AssociationResult.ACCEPTED
|
|
1170
|
+
resultDiagnosticValue = AcseServiceUser.NULL
|
|
1171
|
+
len_ = 0
|
|
1172
|
+
tag = 0
|
|
1173
|
+
while buff.position < len(buff):
|
|
1174
|
+
tag = buff.getUInt8()
|
|
1175
|
+
if tag == BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.APPLICATION_CONTEXT_NAME: # 0xA1
|
|
1176
|
+
# Get length.
|
|
1177
|
+
len_ = buff.getUInt8()
|
|
1178
|
+
if len(buff) - buff.position < len_:
|
|
1179
|
+
raise ValueError("Encoding failed. Not enough data.")
|
|
1180
|
+
if buff.getUInt8() != 0x6:
|
|
1181
|
+
raise ValueError("Encoding failed. Not an Object ID.")
|
|
1182
|
+
if self.settings.isServer and self.settings.cipher:
|
|
1183
|
+
self.settings.cipher.setSecurity(Security.NONE)
|
|
1184
|
+
# Object ID length.
|
|
1185
|
+
len_ = buff.getUInt8()
|
|
1186
|
+
tmp = bytearray(len_)
|
|
1187
|
+
buff.get(tmp)
|
|
1188
|
+
if tmp[:6] != bytearray(b'\x60\x85\x74\x05\x08\x01'):
|
|
1189
|
+
raise Exception("Encoding failed. Invalid Application context name.")
|
|
1190
|
+
match tmp[6], self.settings.getUseLogicalNameReferencing():
|
|
1191
|
+
case 1 | 3, True: pass
|
|
1192
|
+
case 2 | 4, False: pass
|
|
1193
|
+
case _: raise GXDLMSException(AssociationResult.REJECTED_PERMANENT, AcseServiceUser.APPLICATION_CONTEXT_NAME_NOT_SUPPORTED)
|
|
1194
|
+
elif tag == BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.CALLED_AP_TITLE: # 0xA2
|
|
1195
|
+
# Get length.
|
|
1196
|
+
if buff.getUInt8() != 3:
|
|
1197
|
+
raise ValueError("Invalid tag.")
|
|
1198
|
+
if self.settings.isServer:
|
|
1199
|
+
# Choice for result (INTEGER, universal)
|
|
1200
|
+
if buff.getUInt8() != BerType.OCTET_STRING:
|
|
1201
|
+
raise ValueError("Invalid tag.")
|
|
1202
|
+
len_ = buff.getUInt8()
|
|
1203
|
+
tmp = bytearray(len_)
|
|
1204
|
+
buff.get(tmp)
|
|
1205
|
+
try:
|
|
1206
|
+
self.settings.sourceSystemTitle = tmp
|
|
1207
|
+
except Exception as ex:
|
|
1208
|
+
raise ex
|
|
1209
|
+
else:
|
|
1210
|
+
# Choice for result (INTEGER, universal)
|
|
1211
|
+
if buff.getUInt8() != BerType.INTEGER:
|
|
1212
|
+
raise ValueError("Invalid tag.")
|
|
1213
|
+
# Get length.
|
|
1214
|
+
if buff.getUInt8() != 1:
|
|
1215
|
+
raise ValueError("Invalid tag.")
|
|
1216
|
+
resultComponent = AssociationResult(buff.getUInt8())
|
|
1217
|
+
elif tag == BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.CALLED_AE_QUALIFIER: # 0xA3
|
|
1218
|
+
tag = int()
|
|
1219
|
+
resultDiagnosticValue = AcseServiceUser.NULL
|
|
1220
|
+
len_ = buff.getUInt8()
|
|
1221
|
+
# ACSE service user tag.
|
|
1222
|
+
tag = buff.getUInt8()
|
|
1223
|
+
len_ = buff.getUInt8()
|
|
1224
|
+
if self.settings.isServer:
|
|
1225
|
+
calledAEQualifier = bytearray(len_)
|
|
1226
|
+
buff.get(calledAEQualifier)
|
|
1227
|
+
else:
|
|
1228
|
+
# Result source diagnostic component.
|
|
1229
|
+
tag = buff.getUInt8()
|
|
1230
|
+
if tag != BerType.INTEGER:
|
|
1231
|
+
raise ValueError("Invalid tag.")
|
|
1232
|
+
len_ = buff.getUInt8()
|
|
1233
|
+
if len_ != 1:
|
|
1234
|
+
raise ValueError("Invalid tag.")
|
|
1235
|
+
resultDiagnosticValue = AcseServiceUser(buff.getUInt8())
|
|
1236
|
+
elif tag == BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.CALLED_AP_INVOCATION_ID: # 0xA4
|
|
1237
|
+
if self.settings.isServer:
|
|
1238
|
+
# Get len.
|
|
1239
|
+
if buff.getUInt8() != 3:
|
|
1240
|
+
raise ValueError("Invalid tag.")
|
|
1241
|
+
# Choice for result (Universal, Octetstring type)
|
|
1242
|
+
if buff.getUInt8() != BerType.INTEGER:
|
|
1243
|
+
raise ValueError("Invalid tag.")
|
|
1244
|
+
if buff.getUInt8() != 1:
|
|
1245
|
+
raise ValueError("Invalid tag length.")
|
|
1246
|
+
# Get value.
|
|
1247
|
+
len_ = buff.getUInt8()
|
|
1248
|
+
else:
|
|
1249
|
+
# Get length.
|
|
1250
|
+
if buff.getUInt8() != 0xA:
|
|
1251
|
+
raise ValueError("Invalid tag.")
|
|
1252
|
+
# Choice for result (Universal, Octet string type)
|
|
1253
|
+
if buff.getUInt8() != BerType.OCTET_STRING:
|
|
1254
|
+
raise ValueError("Invalid tag.")
|
|
1255
|
+
# responding-AP-title-field
|
|
1256
|
+
# Get length.
|
|
1257
|
+
len_ = buff.getUInt8()
|
|
1258
|
+
tmp = bytearray(len_)
|
|
1259
|
+
buff.get(tmp)
|
|
1260
|
+
self.settings.setSourceSystemTitle(tmp)
|
|
1261
|
+
elif tag == BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.CALLED_AE_INVOCATION_ID: # 0xA5
|
|
1262
|
+
len_ = buff.getUInt8()
|
|
1263
|
+
tag = buff.getUInt8()
|
|
1264
|
+
len_ = buff.getUInt8()
|
|
1265
|
+
self.settings.userId = buff.getUInt8()
|
|
1266
|
+
elif tag == BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.CALLING_AP_TITLE: # 0xA6
|
|
1267
|
+
len_ = buff.getUInt8()
|
|
1268
|
+
tag = buff.getUInt8()
|
|
1269
|
+
len_ = buff.getUInt8()
|
|
1270
|
+
tmp = bytearray(len_)
|
|
1271
|
+
buff.get(tmp)
|
|
1272
|
+
try:
|
|
1273
|
+
self.settings.setSourceSystemTitle(tmp)
|
|
1274
|
+
except Exception as ex:
|
|
1275
|
+
raise ex
|
|
1276
|
+
elif tag == BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.SENDER_ACSE_REQUIREMENTS: # 0xAA
|
|
1277
|
+
len_ = buff.getUInt8()
|
|
1278
|
+
tag = buff.getUInt8()
|
|
1279
|
+
len_ = buff.getUInt8()
|
|
1280
|
+
tmp = bytearray(len_)
|
|
1281
|
+
buff.get(tmp)
|
|
1282
|
+
self.settings.setStoCChallenge(tmp)
|
|
1283
|
+
elif tag == BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.CALLING_AE_QUALIFIER: # 0xA7
|
|
1284
|
+
len_ = buff.getUInt8()
|
|
1285
|
+
tag = buff.getUInt8()
|
|
1286
|
+
len_ = buff.getUInt8()
|
|
1287
|
+
self.settings.userId = buff.getUInt8()
|
|
1288
|
+
elif tag == BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.CALLING_AP_INVOCATION_ID: # 0xA8
|
|
1289
|
+
if buff.getUInt8() != 3:
|
|
1290
|
+
raise ValueError("Invalid tag.")
|
|
1291
|
+
if buff.getUInt8() != 2:
|
|
1292
|
+
raise ValueError("Invalid length.")
|
|
1293
|
+
if buff.getUInt8() != 1:
|
|
1294
|
+
raise ValueError("Invalid tag length.")
|
|
1295
|
+
# Get value.
|
|
1296
|
+
len_ = buff.getUInt8()
|
|
1297
|
+
elif tag == BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.CALLING_AE_INVOCATION_ID: # 0xA9
|
|
1298
|
+
len_ = buff.getUInt8()
|
|
1299
|
+
tag = buff.getUInt8()
|
|
1300
|
+
len_ = buff.getUInt8()
|
|
1301
|
+
self.settings.userId = buff.getUInt8()
|
|
1302
|
+
elif tag in (BerType.CONTEXT | AARQapdu.SENDER_ACSE_REQUIREMENTS, BerType.CONTEXT | AARQapdu.CALLING_AP_INVOCATION_ID): # 0x88
|
|
1303
|
+
# Get sender ACSE-requirements field component.
|
|
1304
|
+
if buff.getUInt8() != 2:
|
|
1305
|
+
raise ValueError("Invalid tag.")
|
|
1306
|
+
if buff.getUInt8() != BerType.OBJECT_DESCRIPTOR:
|
|
1307
|
+
raise ValueError("Invalid tag.")
|
|
1308
|
+
# Get only value because client application is
|
|
1309
|
+
# sending system title with LOW authentication.
|
|
1310
|
+
buff.getUInt8()
|
|
1311
|
+
elif tag in (BerType.CONTEXT | AARQapdu.MECHANISM_NAME, BerType.CONTEXT | AARQapdu.CALLING_AE_INVOCATION_ID): # 0x89
|
|
1312
|
+
ch = buff.getUInt8()
|
|
1313
|
+
if buff.getUInt8() != 0x60:
|
|
1314
|
+
raise ValueError("Invalid tag.")
|
|
1315
|
+
if buff.getUInt8() != 0x85:
|
|
1316
|
+
raise ValueError("Invalid tag.")
|
|
1317
|
+
if buff.getUInt8() != 0x74:
|
|
1318
|
+
raise ValueError("Invalid tag.")
|
|
1319
|
+
if buff.getUInt8() != 0x05:
|
|
1320
|
+
raise ValueError("Invalid tag.")
|
|
1321
|
+
if buff.getUInt8() != 0x08:
|
|
1322
|
+
raise ValueError("Invalid tag.")
|
|
1323
|
+
if buff.getUInt8() != 0x02:
|
|
1324
|
+
raise ValueError("Invalid tag.")
|
|
1325
|
+
ch = buff.getUInt8()
|
|
1326
|
+
self.m_id.set(ch) # TODO: maybe check with current?
|
|
1327
|
+
elif tag == BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.CALLING_AUTHENTICATION_VALUE: # 0xAC
|
|
1328
|
+
len_ = buff.getUInt8()
|
|
1329
|
+
# Get authentication information.
|
|
1330
|
+
if buff.getUInt8() != 0x80:
|
|
1331
|
+
raise ValueError("Invalid tag.")
|
|
1332
|
+
len_ = buff.getUInt8()
|
|
1333
|
+
tmp = bytearray(len_)
|
|
1334
|
+
buff.get(tmp)
|
|
1335
|
+
match self.m_id:
|
|
1336
|
+
case mechanism_id.LOW: self.settings.password = tmp
|
|
1337
|
+
case _: self.settings.ctoSChallenge = tmp
|
|
1338
|
+
elif tag == BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.USER_INFORMATION: # 0xBE
|
|
1339
|
+
# Check result component. Some meters are returning invalid user-information if connection failed.
|
|
1340
|
+
# if resultComponent != AssociationResult.ACCEPTED and resultDiagnosticValue != SourceDiagnostic.NONE:
|
|
1341
|
+
# raise exc.AssociationResultError(resultComponent)
|
|
1342
|
+
try:
|
|
1343
|
+
len_ = buff.getUInt8()
|
|
1344
|
+
if len(buff) - buff.position < len_:
|
|
1345
|
+
raise ValueError("Not enough data.")
|
|
1346
|
+
# Encoding the choice for user information
|
|
1347
|
+
tag = buff.getUInt8()
|
|
1348
|
+
if tag != 0x4:
|
|
1349
|
+
raise ValueError("Invalid tag.")
|
|
1350
|
+
len_ = buff.getUInt8()
|
|
1351
|
+
if len(buff) - buff.position < len_:
|
|
1352
|
+
raise ValueError("Not enough data.")
|
|
1353
|
+
# Tag for xDLMS-Initate.response
|
|
1354
|
+
tag = buff.getUInt8()
|
|
1355
|
+
originalPos = 0
|
|
1356
|
+
if tag in (XDLMSAPDU.GLO_INITIATE_RESPONSE, XDLMSAPDU.GLO_INITIATE_REQUEST,
|
|
1357
|
+
XDLMSAPDU.GENERAL_GLO_CIPHERING, XDLMSAPDU.GENERAL_DED_CIPHERING):
|
|
1358
|
+
buff.position = buff.position - 1
|
|
1359
|
+
p = AesGcmParameter(0, self.settings.sourceSystemTitle, self.settings.cipher.blockCipherKey, self.settings.cipher.authenticationKey)
|
|
1360
|
+
tmp = GXCiphering.decrypt(self.settings.cipher, p, buff)
|
|
1361
|
+
buff.size = 0
|
|
1362
|
+
buff.set(tmp)
|
|
1363
|
+
self.settings.cipher.security = p.security
|
|
1364
|
+
self.settings.cipher.securitySuite = p.securitySuite
|
|
1365
|
+
tag = buff.getUInt8()
|
|
1366
|
+
tmp2 = GXByteBuffer()
|
|
1367
|
+
tmp2.setUInt8(0)
|
|
1368
|
+
tag2 = XDLMSAPDU(tag) # TODO: remove it
|
|
1369
|
+
response = tag2 == XDLMSAPDU.INITIATE_RESPONSE
|
|
1370
|
+
if response:
|
|
1371
|
+
# Optional usage field of the negotiated quality of service component
|
|
1372
|
+
tag = buff.getUInt8()
|
|
1373
|
+
if tag != 0:
|
|
1374
|
+
len_ = buff.getUInt8()
|
|
1375
|
+
buff.position = buff.position + len_
|
|
1376
|
+
elif tag2 == XDLMSAPDU.INITIATE_REQUEST:
|
|
1377
|
+
# Optional usage field of the negotiated quality of service component
|
|
1378
|
+
tag = buff.getUInt8()
|
|
1379
|
+
if tag != 0:
|
|
1380
|
+
len_ = buff.getUInt8()
|
|
1381
|
+
tmp = bytearray(len_)
|
|
1382
|
+
buff.get(tmp)
|
|
1383
|
+
if self.settings.cipher:
|
|
1384
|
+
self.settings.cipher.setDedicatedKey(tmp)
|
|
1385
|
+
elif self.settings.cipher:
|
|
1386
|
+
self.settings.cipher.dedicatedKey = None
|
|
1387
|
+
# Optional usage field of the negotiated quality of service component
|
|
1388
|
+
tag = buff.getUInt8()
|
|
1389
|
+
if tag != 0:
|
|
1390
|
+
len_ = buff.getUInt8()
|
|
1391
|
+
# Optional usage field of the proposed quality of service component
|
|
1392
|
+
tag = buff.getUInt8()
|
|
1393
|
+
# Skip if used.
|
|
1394
|
+
if tag != 0:
|
|
1395
|
+
len_ = buff.getUInt8()
|
|
1396
|
+
buff.position = buff.position + len_
|
|
1397
|
+
elif tag2 == XDLMSAPDU.CONFIRMED_SERVICE_ERROR:
|
|
1398
|
+
raise GXDLMSConfirmedServiceError(ConfirmedServiceError(buff.getUInt8()), ServiceError(buff.getUInt8()), buff.getUInt8())
|
|
1399
|
+
else:
|
|
1400
|
+
raise ValueError("Invalid tag.")
|
|
1401
|
+
# Get DLMS version number.
|
|
1402
|
+
if not response:
|
|
1403
|
+
self.settings.dlmsVersion = buff.getUInt8()
|
|
1404
|
+
if self.settings.dlmsVersion != 6:
|
|
1405
|
+
if not self.settings.isServer:
|
|
1406
|
+
raise ValueError("Invalid DLMS version number.")
|
|
1407
|
+
else:
|
|
1408
|
+
if buff.getUInt8() != 6:
|
|
1409
|
+
raise ValueError("Invalid DLMS version number.")
|
|
1410
|
+
# Tag for conformance block
|
|
1411
|
+
tag = buff.getUInt8()
|
|
1412
|
+
if tag != 0x5F:
|
|
1413
|
+
raise ValueError("Invalid tag.")
|
|
1414
|
+
# Old Way...
|
|
1415
|
+
if buff.getUInt8(buff.position) == 0x1F:
|
|
1416
|
+
buff.getUInt8()
|
|
1417
|
+
len_ = buff.getUInt8()
|
|
1418
|
+
# The number of unused bits in the bit string.
|
|
1419
|
+
tag = buff.getUInt8()
|
|
1420
|
+
#getConformanceToArray todo: make better
|
|
1421
|
+
v = _GXCommon.swapBits(buff.getUInt8())
|
|
1422
|
+
v |= _GXCommon.swapBits(buff.getUInt8()) << 8
|
|
1423
|
+
v |= _GXCommon.swapBits(buff.getUInt8()) << 16
|
|
1424
|
+
if self.settings.isServer:
|
|
1425
|
+
self.negotiated_conformance.set(v & self.settings.proposedConformance)
|
|
1426
|
+
else:
|
|
1427
|
+
self.negotiated_conformance.set(v)
|
|
1428
|
+
self.log(logL.INFO, f"SET CONFORMANCE: {self.negotiated_conformance}")
|
|
1429
|
+
if not response:
|
|
1430
|
+
# Proposed max PDU size.
|
|
1431
|
+
pdu = buff.getUInt16()
|
|
1432
|
+
self.settings.maxPduSize = pdu
|
|
1433
|
+
# If client asks too high PDU.
|
|
1434
|
+
if pdu > self.settings.maxServerPDUSize:
|
|
1435
|
+
self.settings.setMaxPduSize = self.settings.maxServerPDUSize
|
|
1436
|
+
else:
|
|
1437
|
+
pdu = buff.getUInt16()
|
|
1438
|
+
if pdu < 64:
|
|
1439
|
+
raise GXDLMSConfirmedServiceError(ConfirmedServiceError.INITIATE_ERROR, ServiceError.SERVICE, Service.PDU_SIZE)
|
|
1440
|
+
# Max PDU size.
|
|
1441
|
+
self.settings.maxPduSize = pdu
|
|
1442
|
+
if response:
|
|
1443
|
+
# VAA Name
|
|
1444
|
+
tag = buff.getUInt16()
|
|
1445
|
+
if tag == 0x0007:
|
|
1446
|
+
if not self.settings.getUseLogicalNameReferencing():
|
|
1447
|
+
raise ValueError("Invalid VAA.")
|
|
1448
|
+
elif tag == 0xFA00:
|
|
1449
|
+
# If SN
|
|
1450
|
+
if self.settings.getUseLogicalNameReferencing():
|
|
1451
|
+
raise ValueError("Invalid VAA.")
|
|
1452
|
+
else:
|
|
1453
|
+
# Unknown VAA.
|
|
1454
|
+
raise ValueError("Invalid VAA.")
|
|
1455
|
+
except Exception:
|
|
1456
|
+
raise GXDLMSException(AssociationResult.REJECTED_PERMANENT, AcseServiceUser.NO_REASON_GIVEN)
|
|
1457
|
+
elif tag == BerType.CONTEXT | AARQapdu.PROTOCOL_VERSION: # 0x80
|
|
1458
|
+
buff.getUInt8()
|
|
1459
|
+
unusedBits = buff.getUInt8()
|
|
1460
|
+
value = buff.getUInt8()
|
|
1461
|
+
sb = _GXCommon.toBitString(value, 8 - unusedBits)
|
|
1462
|
+
self.settings.protocolVersion = sb
|
|
1463
|
+
else:
|
|
1464
|
+
# Unknown tags.
|
|
1465
|
+
self.log(logL.DEB, "Unknown tag: " + str(tag) + ".")
|
|
1466
|
+
if buff.position < len(buff):
|
|
1467
|
+
len_ = buff.getUInt8()
|
|
1468
|
+
buff.position = buff.position + len_
|
|
1469
|
+
# All meters don't send user-information if connection is failed.
|
|
1470
|
+
# For this reason result component is check again.
|
|
1471
|
+
# if resultComponent != AssociationResult.ACCEPTED and resultDiagnosticValue != SourceDiagnostic.NONE:
|
|
1472
|
+
# raise exc.AssociationResultError(resultComponent, resultDiagnosticValue)
|
|
1473
|
+
return resultDiagnosticValue
|
|
1474
|
+
|
|
1475
|
+
def parseAareResponse(self, pdu: bytes) -> AcseServiceUser:
|
|
1476
|
+
""" TODO: need refactoring. Parses the AARE response. Parse method will update the following data: DLMSVersion, MaxReceivePDUSize, UseLogicalNameReferencing, LNSettings or SNSettings,
|
|
1477
|
+
LNSettings or SNSettings will be updated, depending on the referencing, Logical name or Short name.
|
|
1478
|
+
Received data. GXDLMSClient#aarqRequest GXDLMSClient#useLogicalNameReferencing GXDLMSClient#negotiatedConformance GXDLMSClient#proposedConformance """
|
|
1479
|
+
if (ret := self.parseAARE(pdu)) != AcseServiceUser.AUTHENTICATION_REQUIRED:
|
|
1480
|
+
self.level |= OSI.APPLICATION
|
|
1481
|
+
if self.settings.dlmsVersion != 6:
|
|
1482
|
+
raise ValueError("Invalid DLMS version number.")
|
|
1483
|
+
return ret
|
|
1484
|
+
|
|
1485
|
+
def generate_user_information(self, cipher, encryptedData) -> bytes:
|
|
1486
|
+
info = pack('B', BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.USER_INFORMATION)
|
|
1487
|
+
if not cipher or not cipher.isCiphered():
|
|
1488
|
+
# Length for AARQ user field + oding the choice for user-information (Octet STRING, universal)
|
|
1489
|
+
info += b'\x10\x04'
|
|
1490
|
+
i_r: bytes = self.getInitiateRequest()
|
|
1491
|
+
info += pack(F'B{len(i_r)}s', len(i_r), i_r)
|
|
1492
|
+
else:
|
|
1493
|
+
if encryptedData:
|
|
1494
|
+
# Length for AARQ user field
|
|
1495
|
+
info += pack('B', 4 + len(encryptedData))
|
|
1496
|
+
# Tag
|
|
1497
|
+
info += pack('B', BerType.OCTET_STRING)
|
|
1498
|
+
info += pack('B', 2 + len(encryptedData))
|
|
1499
|
+
# Coding the choice for user-information (Octet STRING,
|
|
1500
|
+
# universal)
|
|
1501
|
+
info += pack('B', XDLMSAPDU.GLO_INITIATE_REQUEST)
|
|
1502
|
+
info += pack('B', len(encryptedData))
|
|
1503
|
+
info += pack(F'{len(encryptedData)}s', encryptedData)
|
|
1504
|
+
else:
|
|
1505
|
+
tmp: bytes = self.getInitiateRequest()
|
|
1506
|
+
p = AesGcmParameter(XDLMSAPDU.GLO_INITIATE_REQUEST, cipher.systemTitle, cipher.blockCipherKey, cipher.authenticationKey)
|
|
1507
|
+
p.security = cipher.security
|
|
1508
|
+
p.invocationCounter = cipher.invocationCounter
|
|
1509
|
+
crypted = bytes(GXCiphering.encrypt(p, tmp))
|
|
1510
|
+
# Length for AARQ user field. Coding the choice for user-information (Octet string, universal)
|
|
1511
|
+
info += pack(F'BBB{len(crypted)}s',
|
|
1512
|
+
2 + len(crypted),
|
|
1513
|
+
BerType.OCTET_STRING,
|
|
1514
|
+
len(crypted),
|
|
1515
|
+
crypted)
|
|
1516
|
+
return info
|
|
1517
|
+
|
|
1518
|
+
def getInitiateRequest(self) -> bytes:
|
|
1519
|
+
"""DLMS UA 1000-2 Ed. 10. 11 AARQ and AARE encoding examples. 11.2 Encoding of the xDLMS InitiateRequest. Todo: rewrite with use UsefullTypes"""
|
|
1520
|
+
info = pack('B', XDLMSAPDU.INITIATE_REQUEST)
|
|
1521
|
+
if not self.settings.cipher or not self.settings.cipher.dedicatedKey:
|
|
1522
|
+
info += b'\x00'
|
|
1523
|
+
else:
|
|
1524
|
+
info += b'\x01' + cdt.encode_length(len(self.settings.cipher.dedicatedKey)) + bytes(self.settings.cipher.dedicatedKey)
|
|
1525
|
+
info += pack(
|
|
1526
|
+
">3B4s3sH",
|
|
1527
|
+
0, # encoding of the response-allowed component (BOOLEAN DEFAULT TRUE) usage flag (FALSE, default value TRUE conveyed)
|
|
1528
|
+
self.quality_of_service,
|
|
1529
|
+
self._objects.dlms_ver if self._objects else self.DEF_DLMS_VER,
|
|
1530
|
+
b'\x5f\x1f\x04\x00', # <5f1f> Tag for conformance block + <04>length of the conformance block + <00> encoding the number of unused bits in the bit string
|
|
1531
|
+
self.proposed_conformance.contents,
|
|
1532
|
+
self.receive_pdu_size)
|
|
1533
|
+
return info
|
|
1534
|
+
|
|
1535
|
+
def aarqRequest(self, m_id: mechanism_id.MechanismIdElement):
|
|
1536
|
+
""" Generate AARQ request. Because all_ meters can't read all_ data in one packet, the packet must be split first, by using SplitDataToPackets method. AARQ request as
|
|
1537
|
+
byte array. @see GXDLMSClient#parseAareResponse """
|
|
1538
|
+
info = bytes()
|
|
1539
|
+
self.settings.resetBlockIndex()
|
|
1540
|
+
self.settings.setStoCChallenge(None)
|
|
1541
|
+
# if self.auto_increase_invoke_ID:
|
|
1542
|
+
# self.settings.setInvokeID(0)
|
|
1543
|
+
# else:
|
|
1544
|
+
# self.settings.setInvokeID(1)
|
|
1545
|
+
# If authentication or ciphering is used.
|
|
1546
|
+
# ProtocolVersion: BerType.CONTEXT | AARQ-apdu.PROTOCOL_VERSION + length(always 2) + unused bites + context
|
|
1547
|
+
if self.protocol_version.encoding != b'\x04\x01\x80':
|
|
1548
|
+
info += pack('2sBc', b'\x80\x02',
|
|
1549
|
+
8 - len(self.protocol_version),
|
|
1550
|
+
self.protocol_version.contents)
|
|
1551
|
+
# Application context name tag. Where A1 - Tag, 09 - content name length, 06 - BerType.OBJECT_IDENTIFIER, 07 - info length
|
|
1552
|
+
info += b'\xa1\x09\x06\x07' + self.APP_CONTEXT_NAME.contents
|
|
1553
|
+
# Add system title.
|
|
1554
|
+
ciphered = self.settings.cipher and self.settings.cipher.isCiphered()
|
|
1555
|
+
if not self.settings.isServer and (ciphered or m_id == mechanism_id.HIGH_GMAC) or m_id == mechanism_id.HIGH_ECDSA:
|
|
1556
|
+
if len(self.settings.cipher.systemTitle) != 8:
|
|
1557
|
+
raise ValueError("SystemTitle")
|
|
1558
|
+
# Add calling-AP-title: BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.CALLING_AP_TITLE + length + BerType.OCTET_STRING + length + systemTitle
|
|
1559
|
+
info += pack(F'cBcB{len(self.settings.cipher.systemTitle)}s',
|
|
1560
|
+
b'\xa6',
|
|
1561
|
+
2 + len(self.settings.cipher.systemTitle),
|
|
1562
|
+
b'\x04',
|
|
1563
|
+
len(self.settings.cipher.systemTitle),
|
|
1564
|
+
self.settings.cipher.systemTitle)
|
|
1565
|
+
# CallingAEInvocationId: BerType.CONTEXT | BerType.CONSTRUCTED | AARQapdu.CALLING_AE_INVOCATION_ID + length + BerType.INTEGER + length + userId
|
|
1566
|
+
if not self.settings.isServer and self.settings.userId != -1:
|
|
1567
|
+
info += pack(F'4sB',
|
|
1568
|
+
b'\xa9\x03\x02\x01',
|
|
1569
|
+
self.settings.userId)
|
|
1570
|
+
# Retrieves the string that indicates the level of authentication, if any.
|
|
1571
|
+
if m_id != mechanism_id.NONE or (self.settings.cipher and self.settings.cipher.security != Security.NONE):
|
|
1572
|
+
info += b'\x8a\x02\x07\x80'
|
|
1573
|
+
# Where: 8b - Tag(CONTEXT(0x80) + AARQ-apdu.MECHANISM_NAME(0x0b)), 07 - info length
|
|
1574
|
+
info += b'\x8b\x07' + AuthenticationMechanismName.get_AARQ_mechanism_name(
|
|
1575
|
+
cryptographic=2,
|
|
1576
|
+
algorithm_id=int(m_id))
|
|
1577
|
+
# Add Calling authentication information.
|
|
1578
|
+
if m_id != mechanism_id.NONE:
|
|
1579
|
+
if m_id == mechanism_id.LOW:
|
|
1580
|
+
c_a_v = self.secret
|
|
1581
|
+
""" calling-authentication-value """
|
|
1582
|
+
elif m_id == mechanism_id.HIGH:
|
|
1583
|
+
self.settings.ctoSChallenge = os.urandom(16)
|
|
1584
|
+
c_a_v = self.settings.ctoSChallenge
|
|
1585
|
+
else:
|
|
1586
|
+
# TODO: must be 8..64 bytes length of urandom for different auth level
|
|
1587
|
+
self.settings.ctoSChallenge = os.urandom(16)
|
|
1588
|
+
c_a_v = self.settings.ctoSChallenge
|
|
1589
|
+
# BerType.CONTEXT | BerType.CONSTRUCTED | AARQ-apdu.CALLING_AUTHENTICATION_VALUE + length + context + info_len
|
|
1590
|
+
info += pack(F'cBBB{len(c_a_v)}s',
|
|
1591
|
+
b'\xac',
|
|
1592
|
+
2 + len(c_a_v),
|
|
1593
|
+
BerType.CONTEXT,
|
|
1594
|
+
len(c_a_v),
|
|
1595
|
+
c_a_v)
|
|
1596
|
+
u_i = self.generate_user_information(self.settings.cipher, None)
|
|
1597
|
+
info = pack('BB', BerType.APPLICATION | BerType.CONSTRUCTED,
|
|
1598
|
+
len(info + u_i)) + info + u_i
|
|
1599
|
+
p = GXDLMSLNParameters(self.settings, 0, ACSEAPDU.AARQ, 0, info, None, 0xff)
|
|
1600
|
+
return self.getLnMessages(p)
|
|
1601
|
+
|
|
1602
|
+
def getLnMessages(self, p: GXDLMSLNParameters):
|
|
1603
|
+
reply = GXByteBuffer()
|
|
1604
|
+
messages = []
|
|
1605
|
+
frame_ = 0
|
|
1606
|
+
if (
|
|
1607
|
+
p.command == XDLMSAPDU.DATA_NOTIFICATION
|
|
1608
|
+
or p.command == XDLMSAPDU.EVENT_NOTIFICATION_REQUEST
|
|
1609
|
+
):
|
|
1610
|
+
frame_ = 0x13
|
|
1611
|
+
while True:
|
|
1612
|
+
# """ Get next logical name PDU. @param p LN parameters. @param reply Generated message. """
|
|
1613
|
+
ciphering = (
|
|
1614
|
+
p.command != ACSEAPDU.AARQ
|
|
1615
|
+
and p.command != ACSEAPDU.AARE
|
|
1616
|
+
and self.settings.cipher
|
|
1617
|
+
and self.settings.cipher.security != Security.NONE
|
|
1618
|
+
)
|
|
1619
|
+
len_ = 0
|
|
1620
|
+
if p.command == ACSEAPDU.AARQ:
|
|
1621
|
+
if (
|
|
1622
|
+
self.settings.gateway
|
|
1623
|
+
and self.settings.gateway.physicalDeviceAddress
|
|
1624
|
+
):
|
|
1625
|
+
reply.setUInt8(XDLMSAPDU.GATEWAY_REQUEST)
|
|
1626
|
+
reply.setUInt8(self.settings.gateway.networkId)
|
|
1627
|
+
reply.setUInt8(len(self.settings.gateway.physicalDeviceAddress))
|
|
1628
|
+
reply.set(self.settings.gateway.physicalDeviceAddress)
|
|
1629
|
+
reply.set(p.attributeDescriptor)
|
|
1630
|
+
else:
|
|
1631
|
+
if p.command != XDLMSAPDU.GENERAL_BLOCK_TRANSFER:
|
|
1632
|
+
reply.setUInt8(p.command)
|
|
1633
|
+
if p.command in (XDLMSAPDU.EVENT_NOTIFICATION_REQUEST, XDLMSAPDU.DATA_NOTIFICATION, XDLMSAPDU.ACCESS_REQUEST, XDLMSAPDU.ACCESS_RESPONSE):
|
|
1634
|
+
if p.command != XDLMSAPDU.EVENT_NOTIFICATION_REQUEST:
|
|
1635
|
+
if p.invokeId != 0:
|
|
1636
|
+
reply.setUInt32(p.invokeId)
|
|
1637
|
+
else:
|
|
1638
|
+
reply.setUInt32(GXDLMS.getLongInvokeIDPriority(self.settings))
|
|
1639
|
+
if p.time is None:
|
|
1640
|
+
reply.setUInt8(cdt.NullData.TAG)
|
|
1641
|
+
else:
|
|
1642
|
+
pos = len(reply)
|
|
1643
|
+
_GXCommon.setData(self.settings, reply, cdt.OctetString.TAG, p.getTime())
|
|
1644
|
+
if p.command != XDLMSAPDU.EVENT_NOTIFICATION_REQUEST:
|
|
1645
|
+
reply.move(pos + 1, pos, len(reply) - pos - 1)
|
|
1646
|
+
GXDLMS.multipleBlocks(p, reply, ciphering)
|
|
1647
|
+
elif p.command != ACSEAPDU.RLRQ:
|
|
1648
|
+
if (
|
|
1649
|
+
p.command != XDLMSAPDU.GET_REQUEST
|
|
1650
|
+
and p.data
|
|
1651
|
+
and reply
|
|
1652
|
+
):
|
|
1653
|
+
GXDLMS.multipleBlocks(p, reply, ciphering)
|
|
1654
|
+
if p.command == XDLMSAPDU.SET_REQUEST:
|
|
1655
|
+
if (
|
|
1656
|
+
p.multipleBlocks
|
|
1657
|
+
and not self.negotiated_conformance.general_block_transfer
|
|
1658
|
+
):
|
|
1659
|
+
if p.requestType == 1:
|
|
1660
|
+
p.requestType = SetRequest.SET_REQUEST_FIRST_DATABLOCK
|
|
1661
|
+
elif p.requestType == 2:
|
|
1662
|
+
p.requestType = SetRequest.SET_REQUEST_WITH_DATABLOCK
|
|
1663
|
+
if p.command == XDLMSAPDU.GET_RESPONSE:
|
|
1664
|
+
if (
|
|
1665
|
+
p.multipleBlocks
|
|
1666
|
+
and not self.negotiated_conformance.general_block_transfer
|
|
1667
|
+
):
|
|
1668
|
+
if p.requestType == 1:
|
|
1669
|
+
p.requestType = 2
|
|
1670
|
+
if p.command != XDLMSAPDU.GENERAL_BLOCK_TRANSFER:
|
|
1671
|
+
reply.setUInt8(p.requestType)
|
|
1672
|
+
if p.invokeId != 0:
|
|
1673
|
+
reply.setUInt8(p.invokeId)
|
|
1674
|
+
else:
|
|
1675
|
+
reply.setUInt8(GXDLMS.getInvokeIDPriority(self.settings))
|
|
1676
|
+
reply.set(p.attributeDescriptor)
|
|
1677
|
+
if (
|
|
1678
|
+
self.settings.is_multiple_block()
|
|
1679
|
+
and self.negotiated_conformance.general_block_transfer
|
|
1680
|
+
):
|
|
1681
|
+
if p.lastBlock:
|
|
1682
|
+
reply.setUInt8(1)
|
|
1683
|
+
self.settings.setCount(0)
|
|
1684
|
+
self.settings.setIndex(0)
|
|
1685
|
+
else:
|
|
1686
|
+
reply.setUInt8(0)
|
|
1687
|
+
reply.setUInt32(p.blockIndex)
|
|
1688
|
+
p.blockIndex += 1
|
|
1689
|
+
if p.status != 0xFF:
|
|
1690
|
+
if (
|
|
1691
|
+
p.status != 0
|
|
1692
|
+
and p.command == XDLMSAPDU.GET_RESPONSE
|
|
1693
|
+
):
|
|
1694
|
+
reply.setUInt8(1)
|
|
1695
|
+
reply.setUInt8(p.status)
|
|
1696
|
+
if p.data:
|
|
1697
|
+
len_ = p.data.size - p.data.position
|
|
1698
|
+
else:
|
|
1699
|
+
len_ = 0
|
|
1700
|
+
totalLength = len_ + len(reply)
|
|
1701
|
+
if ciphering:
|
|
1702
|
+
totalLength += GXDLMS._CIPHERING_HEADER_SIZE
|
|
1703
|
+
if totalLength > self.settings.maxPduSize:
|
|
1704
|
+
len_ = self.settings.maxPduSize - len(reply)
|
|
1705
|
+
if ciphering:
|
|
1706
|
+
len_ -= GXDLMS._CIPHERING_HEADER_SIZE
|
|
1707
|
+
len_ -= _GXCommon.getObjectCountSizeInBytes(len_)
|
|
1708
|
+
_GXCommon.setObjectCount(len_, reply)
|
|
1709
|
+
reply.set(p.data, len_)
|
|
1710
|
+
if len_ == 0:
|
|
1711
|
+
if (
|
|
1712
|
+
p.status != 0xFF
|
|
1713
|
+
and p.command != XDLMSAPDU.GENERAL_BLOCK_TRANSFER
|
|
1714
|
+
):
|
|
1715
|
+
if (
|
|
1716
|
+
p.status != 0
|
|
1717
|
+
and p.command == XDLMSAPDU.GET_RESPONSE
|
|
1718
|
+
):
|
|
1719
|
+
reply.setUInt8(1)
|
|
1720
|
+
reply.setUInt8(p.status)
|
|
1721
|
+
if p.data:
|
|
1722
|
+
len_ = p.data.size - p.data.position
|
|
1723
|
+
if self.settings.gateway and self.settings.gateway.physicalDeviceAddress:
|
|
1724
|
+
if 3 + len_ + len(self.settings.gateway.physicalDeviceAddress) > self.settings.maxPduSize:
|
|
1725
|
+
len_ -= (3 + len(self.settings.gateway.physicalDeviceAddress))
|
|
1726
|
+
tmp = GXByteBuffer(reply)
|
|
1727
|
+
reply.size = 0
|
|
1728
|
+
reply.setUInt8(XDLMSAPDU.GATEWAY_REQUEST)
|
|
1729
|
+
reply.setUInt8(self.settings.gateway.networkId)
|
|
1730
|
+
reply.setUInt8(len(self.settings.gateway.physicalDeviceAddress))
|
|
1731
|
+
reply.set(self.settings.gateway.physicalDeviceAddress)
|
|
1732
|
+
reply.set(tmp)
|
|
1733
|
+
if self.negotiated_conformance.general_block_transfer:
|
|
1734
|
+
if 7 + len_ + len(reply) > self.settings.maxPduSize:
|
|
1735
|
+
len_ = self.settings.maxPduSize - len(reply) - 7
|
|
1736
|
+
if (
|
|
1737
|
+
ciphering
|
|
1738
|
+
and p.command != XDLMSAPDU.GENERAL_BLOCK_TRANSFER
|
|
1739
|
+
):
|
|
1740
|
+
reply.set(p.data)
|
|
1741
|
+
tmp = []
|
|
1742
|
+
if self.settings.cipher.securitySuite == SecuritySuite.AES_GCM_128_AUT_ENCR_AND_AES_128_KEY_WRAP:
|
|
1743
|
+
tmp = self.cipher0(p, reply)
|
|
1744
|
+
p.data.size = 0
|
|
1745
|
+
p.data.set(tmp)
|
|
1746
|
+
reply.size = 0
|
|
1747
|
+
len_ = p.data.size
|
|
1748
|
+
if 7 + len_ > self.settings.maxPduSize:
|
|
1749
|
+
len_ = self.settings.maxPduSize - 7
|
|
1750
|
+
ciphering = False
|
|
1751
|
+
elif (
|
|
1752
|
+
p.command != XDLMSAPDU.GET_REQUEST
|
|
1753
|
+
and len_ + len(reply) > self.settings.maxPduSize
|
|
1754
|
+
):
|
|
1755
|
+
len_ = self.settings.maxPduSize - len(reply)
|
|
1756
|
+
reply.set(p.data, p.data.position, len_)
|
|
1757
|
+
elif (
|
|
1758
|
+
(
|
|
1759
|
+
self.settings.gateway
|
|
1760
|
+
and self.settings.gateway.physicalDeviceAddress
|
|
1761
|
+
)
|
|
1762
|
+
and not (
|
|
1763
|
+
p.command == XDLMSAPDU.GENERAL_BLOCK_TRANSFER
|
|
1764
|
+
or (
|
|
1765
|
+
p.multipleBlocks
|
|
1766
|
+
and self.negotiated_conformance.general_block_transfer
|
|
1767
|
+
)
|
|
1768
|
+
)
|
|
1769
|
+
):
|
|
1770
|
+
if 3 + len_ + len(self.settings.gateway.physicalDeviceAddress) > self.settings.maxPduSize:
|
|
1771
|
+
len_ -= (3 + len(self.settings.gateway.physicalDeviceAddress))
|
|
1772
|
+
tmp = GXByteBuffer(reply)
|
|
1773
|
+
reply.size = 0
|
|
1774
|
+
reply.setUInt8(XDLMSAPDU.GATEWAY_REQUEST)
|
|
1775
|
+
reply.setUInt8(self.settings.gateway.networkId)
|
|
1776
|
+
reply.setUInt8(len(self.settings.gateway.physicalDeviceAddress))
|
|
1777
|
+
reply.set(self.settings.gateway.physicalDeviceAddress)
|
|
1778
|
+
reply.set(tmp)
|
|
1779
|
+
if (
|
|
1780
|
+
ciphering
|
|
1781
|
+
and reply
|
|
1782
|
+
and not self.negotiated_conformance.general_block_transfer
|
|
1783
|
+
and p.command != XDLMSAPDU.RELEASE_REQUEST
|
|
1784
|
+
):
|
|
1785
|
+
tmp = []
|
|
1786
|
+
if self.settings.cipher.securitySuite == SecuritySuite.AES_GCM_128_AUT_ENCR_AND_AES_128_KEY_WRAP:
|
|
1787
|
+
tmp = self.cipher0(p, reply.array())
|
|
1788
|
+
reply.size = 0
|
|
1789
|
+
reply.set(tmp)
|
|
1790
|
+
if (
|
|
1791
|
+
p.command == XDLMSAPDU.GENERAL_BLOCK_TRANSFER
|
|
1792
|
+
or (
|
|
1793
|
+
p.multipleBlocks
|
|
1794
|
+
and self.negotiated_conformance.general_block_transfer
|
|
1795
|
+
)
|
|
1796
|
+
):
|
|
1797
|
+
bb = GXByteBuffer()
|
|
1798
|
+
bb.set(reply)
|
|
1799
|
+
reply.clear()
|
|
1800
|
+
reply.setUInt8(XDLMSAPDU.GENERAL_BLOCK_TRANSFER)
|
|
1801
|
+
if p.lastBlock:
|
|
1802
|
+
value = 0x80
|
|
1803
|
+
elif p.streaming:
|
|
1804
|
+
value = 0x40
|
|
1805
|
+
else:
|
|
1806
|
+
value = 0
|
|
1807
|
+
value |= p.windowSize
|
|
1808
|
+
reply.setUInt8(value)
|
|
1809
|
+
reply.setUInt16(p.blockIndex)
|
|
1810
|
+
p.blockIndex += 1
|
|
1811
|
+
if (
|
|
1812
|
+
p.command != XDLMSAPDU.DATA_NOTIFICATION
|
|
1813
|
+
and p.blockNumberAck != 0
|
|
1814
|
+
):
|
|
1815
|
+
reply.setUInt16(p.blockNumberAck)
|
|
1816
|
+
p.blockNumberAck += 1
|
|
1817
|
+
else:
|
|
1818
|
+
p.blockNumberAck = -1
|
|
1819
|
+
reply.setUInt16(0)
|
|
1820
|
+
_GXCommon.setObjectCount(len(bb), reply)
|
|
1821
|
+
reply.set(bb)
|
|
1822
|
+
if p.command != XDLMSAPDU.GENERAL_BLOCK_TRANSFER:
|
|
1823
|
+
p.command = XDLMSAPDU.GENERAL_BLOCK_TRANSFER
|
|
1824
|
+
p.blockNumberAck += 1
|
|
1825
|
+
if (
|
|
1826
|
+
self.settings.gateway
|
|
1827
|
+
and self.settings.gateway.physicalDeviceAddress
|
|
1828
|
+
):
|
|
1829
|
+
if 3 + len_ + len(self.settings.gateway.physicalDeviceAddress) > self.settings.maxPduSize:
|
|
1830
|
+
len_ -= (3 + len(self.settings.gateway.physicalDeviceAddress))
|
|
1831
|
+
tmp = GXByteBuffer(reply)
|
|
1832
|
+
reply.size = 0
|
|
1833
|
+
reply.setUInt8(XDLMSAPDU.GATEWAY_REQUEST)
|
|
1834
|
+
reply.setUInt8(self.settings.gateway.networkId)
|
|
1835
|
+
reply.setUInt8(len(self.settings.gateway.physicalDeviceAddress))
|
|
1836
|
+
reply.set(self.settings.gateway.physicalDeviceAddress)
|
|
1837
|
+
reply.set(tmp)
|
|
1838
|
+
p.lastBlock = True
|
|
1839
|
+
if p.attributeDescriptor is None:
|
|
1840
|
+
self.settings.increaseBlockIndex()
|
|
1841
|
+
if (
|
|
1842
|
+
p.command == ACSEAPDU.AARQ
|
|
1843
|
+
and p.command == XDLMSAPDU.GET_REQUEST
|
|
1844
|
+
):
|
|
1845
|
+
assert not self.settings.maxPduSize < len(reply)
|
|
1846
|
+
match self.com_profile:
|
|
1847
|
+
case c_pf.TCPUDPIP():
|
|
1848
|
+
messages.append(GXDLMS.getWrapperFrame(self.settings, p.command, reply)) # TODO: rewrite getWrapperFrame with return list[bytes]
|
|
1849
|
+
case c_pf.HDLC():
|
|
1850
|
+
self.add_frames_to_queue(frame.Control(frame_), bytes(reply.array()))
|
|
1851
|
+
case _:
|
|
1852
|
+
raise ValueError("InterfaceType")
|
|
1853
|
+
reply.clear()
|
|
1854
|
+
frame_ = 0
|
|
1855
|
+
if (
|
|
1856
|
+
not p.data
|
|
1857
|
+
or p.data.position == p.data.size
|
|
1858
|
+
):
|
|
1859
|
+
break
|
|
1860
|
+
return messages
|
|
1861
|
+
|
|
1862
|
+
def get_get_request_normal(self, attr_desc: ut.CosemAttributeDescriptor | ut.CosemAttributeDescriptorWithSelection):
|
|
1863
|
+
p = GXDLMSLNParameters(settings=self.settings,
|
|
1864
|
+
invokeId=0,
|
|
1865
|
+
command=XDLMSAPDU.GET_REQUEST,
|
|
1866
|
+
requestType=pdu.GetResponse.NORMAL,
|
|
1867
|
+
attributeDescriptor=GXByteBuffer(attr_desc.contents),
|
|
1868
|
+
data=None,
|
|
1869
|
+
status=0xFF)
|
|
1870
|
+
return self.getLnMessages(p)
|
|
1871
|
+
|
|
1872
|
+
def get_set_request_normal(self, obj: ic.COSEMInterfaceClasses, attr_index: int, value: bytes = None):
|
|
1873
|
+
self.settings.resetBlockIndex()
|
|
1874
|
+
access_selection_parameters = b'\x00'
|
|
1875
|
+
attribute_descriptor = GXByteBuffer(obj.get_attribute_descriptor(attr_index) + access_selection_parameters)
|
|
1876
|
+
data = GXByteBuffer()
|
|
1877
|
+
if value:
|
|
1878
|
+
data.set(value)
|
|
1879
|
+
else:
|
|
1880
|
+
attr = obj.get_attr(attr_index)
|
|
1881
|
+
data.set(attr.encoding) # add raw data
|
|
1882
|
+
p = GXDLMSLNParameters(self.settings, 0, XDLMSAPDU.SET_REQUEST, SetRequest.SET_REQUEST_NORMAL, attribute_descriptor, data, 0xff)
|
|
1883
|
+
p.blockIndex = self.settings.blockIndex
|
|
1884
|
+
p.blockNumberAck = self.settings.blockNumberAck
|
|
1885
|
+
p.streaming = False
|
|
1886
|
+
return self.getLnMessages(p)
|
|
1887
|
+
|
|
1888
|
+
def get_set_request_normal2(self, attr_desc: ut.CosemAttributeDescriptor, value: cdt.CommonDataTypes):
|
|
1889
|
+
self.settings.resetBlockIndex()
|
|
1890
|
+
attribute_descriptor = GXByteBuffer(attr_desc.contents)
|
|
1891
|
+
data = GXByteBuffer(value.encoding)
|
|
1892
|
+
p = GXDLMSLNParameters(self.settings, 0, XDLMSAPDU.SET_REQUEST, SetRequest.SET_REQUEST_NORMAL, attribute_descriptor, data, 0xff)
|
|
1893
|
+
p.blockIndex = self.settings.blockIndex
|
|
1894
|
+
p.blockNumberAck = self.settings.blockNumberAck
|
|
1895
|
+
p.streaming = False
|
|
1896
|
+
return self.getLnMessages(p)
|
|
1897
|
+
|
|
1898
|
+
@deprecated("use get_action_request_normal")
|
|
1899
|
+
def get_action_request_normal_old(self, meth_desc: ut.CosemMethodDescriptor):
|
|
1900
|
+
self.settings.resetBlockIndex()
|
|
1901
|
+
method = self.objects.get_object(meth_desc).get_meth(int(meth_desc.method_id))
|
|
1902
|
+
method_invocation_parameters = GXByteBuffer(cdt.Boolean(b'\x03' + method.TAG).contents + method.encoding)
|
|
1903
|
+
method_descriptor = GXByteBuffer(meth_desc.contents)
|
|
1904
|
+
p = GXDLMSLNParameters(self.settings, 0, XDLMSAPDU.ACTION_REQUEST, ActionRequest.NORMAL, method_descriptor, method_invocation_parameters, 0xff)
|
|
1905
|
+
return self.getLnMessages(p)
|
|
1906
|
+
|
|
1907
|
+
def get_action_request_normal(self, meth_desc: ut.CosemMethodDescriptor, method: cdt.CommonDataType):
|
|
1908
|
+
"""method: specific method"""
|
|
1909
|
+
self.settings.resetBlockIndex()
|
|
1910
|
+
method_invocation_parameters = GXByteBuffer(cdt.Boolean(b'\x03' + method.TAG).contents + method.encoding)
|
|
1911
|
+
method_descriptor = GXByteBuffer(meth_desc.contents)
|
|
1912
|
+
p = GXDLMSLNParameters(self.settings, 0, XDLMSAPDU.ACTION_REQUEST, ActionRequest.NORMAL, method_descriptor, method_invocation_parameters, 0xff)
|
|
1913
|
+
return self.getLnMessages(p)
|
|
1914
|
+
|
|
1915
|
+
def releaseRequest(self):
|
|
1916
|
+
# TODO: rewrite
|
|
1917
|
+
info = b'\x03\x80\x01\x00'
|
|
1918
|
+
if self.use_protected_release:
|
|
1919
|
+
#Increase IC.
|
|
1920
|
+
if self.settings.cipher and self.settings.cipher.isCiphered:
|
|
1921
|
+
self.settings.cipher.invocationCounter = self.settings.cipher.invocationCounter + 1
|
|
1922
|
+
info += self.generate_user_information(self.settings.cipher, None)
|
|
1923
|
+
info = pack('H', len(info)) + info
|
|
1924
|
+
buff = GXByteBuffer(info)
|
|
1925
|
+
if self.settings.getUseLogicalNameReferencing():
|
|
1926
|
+
p = GXDLMSLNParameters(self.settings, 0, ACSEAPDU.RLRQ, 0, buff, None, 0xff)
|
|
1927
|
+
reply = self.getLnMessages(p)
|
|
1928
|
+
else:
|
|
1929
|
+
reply = self.getSnMessages(GXDLMSSNParameters(self.settings, ACSEAPDU.RLRQ, 0xFF, 0xFF, None, buff))
|
|
1930
|
+
self.level -= OSI.APPLICATION
|
|
1931
|
+
return reply
|
|
1932
|
+
|
|
1933
|
+
@classmethod
|
|
1934
|
+
def getGloMessage(cls, command: XDLMSAPDU | ACSEAPDU) -> XDLMSAPDU | ACSEAPDU:
|
|
1935
|
+
""" Get used glo message. Executed command. Integer value of glo message."""
|
|
1936
|
+
match command:
|
|
1937
|
+
case XDLMSAPDU.READ_REQUEST: return XDLMSAPDU.GLO_READ_REQUEST
|
|
1938
|
+
case XDLMSAPDU.GET_REQUEST: return XDLMSAPDU.GLO_GET_REQUEST
|
|
1939
|
+
case XDLMSAPDU.WRITE_REQUEST: return XDLMSAPDU.GLO_WRITE_REQUEST
|
|
1940
|
+
case XDLMSAPDU.SET_REQUEST: return XDLMSAPDU.GLO_SET_REQUEST
|
|
1941
|
+
case XDLMSAPDU.ACTION_REQUEST: return XDLMSAPDU.GLO_ACTION_REQUEST
|
|
1942
|
+
case XDLMSAPDU.READ_RESPONSE: return XDLMSAPDU.GLO_READ_RESPONSE
|
|
1943
|
+
case XDLMSAPDU.GET_RESPONSE: return XDLMSAPDU.GLO_GET_RESPONSE
|
|
1944
|
+
case XDLMSAPDU.WRITE_RESPONSE: return XDLMSAPDU.GLO_WRITE_RESPONSE
|
|
1945
|
+
case XDLMSAPDU.SET_RESPONSE: return XDLMSAPDU.GLO_SET_RESPONSE
|
|
1946
|
+
case XDLMSAPDU.ACTION_RESPONSE: return XDLMSAPDU.GLO_ACTION_RESPONSE
|
|
1947
|
+
case XDLMSAPDU.DATA_NOTIFICATION: return XDLMSAPDU.GENERAL_GLO_CIPHERING
|
|
1948
|
+
case ACSEAPDU.RLRQ: return ACSEAPDU.RLRQ
|
|
1949
|
+
case ACSEAPDU.RLRE: return ACSEAPDU.RLRE
|
|
1950
|
+
case _: raise Exception("Invalid GLO command.")
|
|
1951
|
+
|
|
1952
|
+
@classmethod
|
|
1953
|
+
def getDedMessage(cls, command: XDLMSAPDU | ACSEAPDU) -> XDLMSAPDU | ACSEAPDU:
|
|
1954
|
+
""" Get used ded message. Executed command. Integer value of ded message. """
|
|
1955
|
+
match command:
|
|
1956
|
+
case XDLMSAPDU.GET_REQUEST: return XDLMSAPDU.DED_GET_REQUEST
|
|
1957
|
+
case XDLMSAPDU.SET_REQUEST: return XDLMSAPDU.DED_SET_REQUEST
|
|
1958
|
+
case XDLMSAPDU.ACTION_REQUEST: return XDLMSAPDU.DED_ACTION_REQUEST
|
|
1959
|
+
case XDLMSAPDU.GET_RESPONSE: return XDLMSAPDU.DED_GET_RESPONSE
|
|
1960
|
+
case XDLMSAPDU.SET_RESPONSE: return XDLMSAPDU.DED_SET_RESPONSE
|
|
1961
|
+
case XDLMSAPDU.ACTION_RESPONSE: return XDLMSAPDU.DED_ACTION_RESPONSE
|
|
1962
|
+
case XDLMSAPDU.DATA_NOTIFICATION: return XDLMSAPDU.GENERAL_DED_CIPHERING
|
|
1963
|
+
case ACSEAPDU.RLRQ: return ACSEAPDU.RLRQ
|
|
1964
|
+
case ACSEAPDU.RLRE: return ACSEAPDU.RLRE
|
|
1965
|
+
case _: raise Exception("Invalid DED command.")
|
|
1966
|
+
|
|
1967
|
+
def cipher0(self, p: GXDLMSLNParameters, data: GXByteBuffer):
|
|
1968
|
+
cmd = 0
|
|
1969
|
+
key = None
|
|
1970
|
+
cipher = p.settings.cipher
|
|
1971
|
+
if not self.negotiated_conformance.general_protection:
|
|
1972
|
+
if cipher.dedicatedKey and (OSI.APPLICATION in self.level): # todo: maybe level is wrong
|
|
1973
|
+
cmd = self.getDedMessage(p.command)
|
|
1974
|
+
key = cipher.dedicatedKey
|
|
1975
|
+
else:
|
|
1976
|
+
cmd = self.getGloMessage(p.command)
|
|
1977
|
+
key = cipher.blockCipherKey
|
|
1978
|
+
else:
|
|
1979
|
+
if cipher.dedicatedKey:
|
|
1980
|
+
cmd = XDLMSAPDU.GENERAL_DED_CIPHERING
|
|
1981
|
+
key = cipher.dedicatedKey
|
|
1982
|
+
else:
|
|
1983
|
+
cmd = XDLMSAPDU.GENERAL_GLO_CIPHERING
|
|
1984
|
+
key = cipher.blockCipherKey
|
|
1985
|
+
cipher.invocationCounter = cipher.invocationCounter + 1
|
|
1986
|
+
s = AesGcmParameter(cmd, cipher.systemTitle, key, cipher.authenticationKey)
|
|
1987
|
+
s.ignoreSystemTitle = p.settings.standard == Standard.ITALY
|
|
1988
|
+
s.security = cipher.security
|
|
1989
|
+
s.invocationCounter = cipher.invocationCounter
|
|
1990
|
+
tmp = GXCiphering.encrypt(s, data)
|
|
1991
|
+
if p.command == XDLMSAPDU.DATA_NOTIFICATION or p.command == XDLMSAPDU.GENERAL_GLO_CIPHERING or p.command == XDLMSAPDU.GENERAL_DED_CIPHERING:
|
|
1992
|
+
reply = GXByteBuffer()
|
|
1993
|
+
reply.setUInt8(tmp[0])
|
|
1994
|
+
if p.settings.getStandard() == Standard.ITALY:
|
|
1995
|
+
reply.setUInt8(0)
|
|
1996
|
+
else:
|
|
1997
|
+
_GXCommon.setObjectCount(len(p.settings.cipher.systemTitle), reply)
|
|
1998
|
+
reply.set(p.settings.cipher.systemTitle)
|
|
1999
|
+
reply.set(tmp, 1, len(tmp))
|
|
2000
|
+
return reply.array()
|
|
2001
|
+
return tmp
|
|
2002
|
+
|
|
2003
|
+
@property
|
|
2004
|
+
def current_association(self) -> AssociationLN:
|
|
2005
|
+
return self.objects.sap2association(self.SAP)
|
|
2006
|
+
|
|
2007
|
+
def get_SNRM_request(self):
|
|
2008
|
+
""" Generates SNRM request. his method is used to generate send SNRMRequest. Before the SNRM request can be generated, at least the following properties must be set:
|
|
2009
|
+
ClientAddress, ServerAddress.
|
|
2010
|
+
According to IEC 62056-47: when communicating using TCP/IP, the SNRM request is not send. """
|
|
2011
|
+
self.add_frames_to_queue(control=frame.Control.SNRM_P)
|
|
2012
|
+
|
|
2013
|
+
def add_frames_to_queue(self, control: frame.Control, data: bytes = bytes()):
|
|
2014
|
+
""" Create and set new frames to queue """
|
|
2015
|
+
new_frames: Deque[frame.Frame] = deque()
|
|
2016
|
+
""" frames container """
|
|
2017
|
+
if control == frame.Control.SNRM_P:
|
|
2018
|
+
info = self.com_profile.negotiation.SNRM
|
|
2019
|
+
elif control.is_information():
|
|
2020
|
+
info = sub_layer.LLC(message=data).content
|
|
2021
|
+
""" HDLS info field """
|
|
2022
|
+
else:
|
|
2023
|
+
info = bytes()
|
|
2024
|
+
if len(data) != 0:
|
|
2025
|
+
raise ValueError('Warning DATA not empty, but frame not info')
|
|
2026
|
+
while True:
|
|
2027
|
+
info3 = info[:self.com_profile.negotiation.max_info_transmit]
|
|
2028
|
+
info = info[self.com_profile.negotiation.max_info_transmit:]
|
|
2029
|
+
new_frames.append(frame.Frame(control=control if control != 0 else self.settings.getNextSend(True),
|
|
2030
|
+
DA=self.DA,
|
|
2031
|
+
SA=self.SA,
|
|
2032
|
+
info=info3,
|
|
2033
|
+
is_segmentation=bool(len(info))
|
|
2034
|
+
))
|
|
2035
|
+
if len(info) == 0:
|
|
2036
|
+
break
|
|
2037
|
+
else:
|
|
2038
|
+
control = frame.Control(self.settings.getNextSend(False))
|
|
2039
|
+
self.send_frames.extend(new_frames)
|
|
2040
|
+
|
|
2041
|
+
def __str__(self):
|
|
2042
|
+
if not self._objects or not self._objects.LDN.value:
|
|
2043
|
+
return str(self.id)
|
|
2044
|
+
else:
|
|
2045
|
+
return self._objects.LDN.value.to_str()
|
|
2046
|
+
|
|
2047
|
+
def get_serial_number(self) -> str:
|
|
2048
|
+
""" return serial number as text. If serial object is absence return 'недоступен' """
|
|
2049
|
+
if self._objects is None:
|
|
2050
|
+
return "нет типа"
|
|
2051
|
+
obj = self._objects.serial_number
|
|
2052
|
+
if isinstance(obj, Data) and obj.value is not None:
|
|
2053
|
+
if isinstance(obj.value, cdt.OctetString):
|
|
2054
|
+
return obj.value.to_str()
|
|
2055
|
+
else:
|
|
2056
|
+
return str(obj.value)
|
|
2057
|
+
else:
|
|
2058
|
+
return 'недоступен'
|
|
2059
|
+
|
|
2060
|
+
@deprecated("<use ReadObjAttr>")
|
|
2061
|
+
async def read_attribute(self, obj: ic.COSEMInterfaceClasses | str,
|
|
2062
|
+
attr_index: int):
|
|
2063
|
+
# TODO: redundant, use read_attr?
|
|
2064
|
+
if isinstance(obj, str):
|
|
2065
|
+
obj = self.objects.get_object(obj)
|
|
2066
|
+
self.get_get_request_normal(obj.get_attr_descriptor(
|
|
2067
|
+
value=attr_index,
|
|
2068
|
+
with_selection=bool(self.negotiated_conformance.selective_access)))
|
|
2069
|
+
start_read_time: float = time.perf_counter()
|
|
2070
|
+
data = (await self.read_data_block()).unwrap()
|
|
2071
|
+
self.last_transfer_time = datetime.timedelta(seconds=time.perf_counter()-start_read_time)
|
|
2072
|
+
obj.set_attr(attr_index, data)
|
|
2073
|
+
|
|
2074
|
+
@deprecated("use execute_method2")
|
|
2075
|
+
async def execute_method(self, meth_desc: ut.CosemMethodDescriptor) -> result.Ok | result.Error:
|
|
2076
|
+
data = self.get_action_request_normal_old(meth_desc)
|
|
2077
|
+
return await self.read_data_block()
|
|
2078
|
+
|
|
2079
|
+
async def execute_method2(self, obj: ic.COSEMInterfaceClasses, i: int, mip=None) -> result.Ok | result.Error:
|
|
2080
|
+
data = self.get_action_request_normal(
|
|
2081
|
+
meth_desc=obj.get_meth_descriptor(i),
|
|
2082
|
+
method=obj.get_meth_element(i).DATA_TYPE() if mip is None else mip)
|
|
2083
|
+
return await self.read_data_block()
|
|
2084
|
+
|
|
2085
|
+
async def is_equal_attribute(self, obj: ic.COSEMInterfaceClasses, attr_index: int | str, with_time: bool | datetime.datetime = False) -> bool:
|
|
2086
|
+
self.get_get_request_normal(obj.get_attr_descriptor(attr_index))
|
|
2087
|
+
data = (await self.read_data_block()).unwrap()
|
|
2088
|
+
if obj.get_attr(attr_index).encoding == data:
|
|
2089
|
+
return True
|
|
2090
|
+
else:
|
|
2091
|
+
return False
|