DLMS-SPODES-client 0.19.22__py3-none-any.whl → 0.19.23__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. DLMS_SPODES_client/FCS16.py +39 -39
  2. DLMS_SPODES_client/__init__.py +12 -12
  3. DLMS_SPODES_client/client.py +2091 -2091
  4. DLMS_SPODES_client/gurux_common/enums/TraceLevel.py +21 -21
  5. DLMS_SPODES_client/gurux_dlms/AesGcmParameter.py +37 -37
  6. DLMS_SPODES_client/gurux_dlms/CountType.py +16 -16
  7. DLMS_SPODES_client/gurux_dlms/GXByteBuffer.py +545 -545
  8. DLMS_SPODES_client/gurux_dlms/GXCiphering.py +196 -196
  9. DLMS_SPODES_client/gurux_dlms/GXDLMS.py +426 -426
  10. DLMS_SPODES_client/gurux_dlms/GXDLMSChippering.py +237 -237
  11. DLMS_SPODES_client/gurux_dlms/GXDLMSChipperingStream.py +977 -977
  12. DLMS_SPODES_client/gurux_dlms/GXDLMSConfirmedServiceError.py +90 -90
  13. DLMS_SPODES_client/gurux_dlms/GXDLMSException.py +139 -139
  14. DLMS_SPODES_client/gurux_dlms/GXDLMSLNParameters.py +33 -33
  15. DLMS_SPODES_client/gurux_dlms/GXDLMSSNParameters.py +21 -21
  16. DLMS_SPODES_client/gurux_dlms/GXDLMSSettings.py +254 -254
  17. DLMS_SPODES_client/gurux_dlms/GXReplyData.py +87 -87
  18. DLMS_SPODES_client/gurux_dlms/HdlcControlFrame.py +9 -9
  19. DLMS_SPODES_client/gurux_dlms/MBusCommand.py +8 -8
  20. DLMS_SPODES_client/gurux_dlms/MBusEncryptionMode.py +27 -27
  21. DLMS_SPODES_client/gurux_dlms/ResponseType.py +8 -8
  22. DLMS_SPODES_client/gurux_dlms/SetResponseType.py +29 -29
  23. DLMS_SPODES_client/gurux_dlms/_HDLCInfo.py +9 -9
  24. DLMS_SPODES_client/gurux_dlms/__init__.py +75 -75
  25. DLMS_SPODES_client/gurux_dlms/enums/Access.py +12 -12
  26. DLMS_SPODES_client/gurux_dlms/enums/ApplicationReference.py +14 -14
  27. DLMS_SPODES_client/gurux_dlms/enums/Authentication.py +41 -41
  28. DLMS_SPODES_client/gurux_dlms/enums/BerType.py +35 -35
  29. DLMS_SPODES_client/gurux_dlms/enums/Command.py +285 -285
  30. DLMS_SPODES_client/gurux_dlms/enums/Definition.py +9 -9
  31. DLMS_SPODES_client/gurux_dlms/enums/ErrorCode.py +46 -46
  32. DLMS_SPODES_client/gurux_dlms/enums/ExceptionServiceError.py +12 -12
  33. DLMS_SPODES_client/gurux_dlms/enums/HardwareResource.py +10 -10
  34. DLMS_SPODES_client/gurux_dlms/enums/HdlcFrameType.py +9 -9
  35. DLMS_SPODES_client/gurux_dlms/enums/Initiate.py +10 -10
  36. DLMS_SPODES_client/gurux_dlms/enums/LoadDataSet.py +13 -13
  37. DLMS_SPODES_client/gurux_dlms/enums/ObjectType.py +306 -306
  38. DLMS_SPODES_client/gurux_dlms/enums/Priority.py +7 -7
  39. DLMS_SPODES_client/gurux_dlms/enums/RequestTypes.py +9 -9
  40. DLMS_SPODES_client/gurux_dlms/enums/Security.py +14 -14
  41. DLMS_SPODES_client/gurux_dlms/enums/Service.py +16 -16
  42. DLMS_SPODES_client/gurux_dlms/enums/ServiceClass.py +9 -9
  43. DLMS_SPODES_client/gurux_dlms/enums/ServiceError.py +8 -8
  44. DLMS_SPODES_client/gurux_dlms/enums/Standard.py +18 -18
  45. DLMS_SPODES_client/gurux_dlms/enums/StateError.py +7 -7
  46. DLMS_SPODES_client/gurux_dlms/enums/Task.py +10 -10
  47. DLMS_SPODES_client/gurux_dlms/enums/VdeStateError.py +10 -10
  48. DLMS_SPODES_client/gurux_dlms/enums/__init__.py +33 -33
  49. DLMS_SPODES_client/gurux_dlms/internal/_GXCommon.py +1673 -1673
  50. DLMS_SPODES_client/logger.py +56 -56
  51. DLMS_SPODES_client/services.py +97 -97
  52. DLMS_SPODES_client/session.py +365 -365
  53. DLMS_SPODES_client/settings.py +48 -48
  54. DLMS_SPODES_client/task.py +1842 -1842
  55. {dlms_spodes_client-0.19.22.dist-info → dlms_spodes_client-0.19.23.dist-info}/METADATA +29 -27
  56. dlms_spodes_client-0.19.23.dist-info/RECORD +61 -0
  57. dlms_spodes_client-0.19.22.dist-info/RECORD +0 -61
  58. {dlms_spodes_client-0.19.22.dist-info → dlms_spodes_client-0.19.23.dist-info}/WHEEL +0 -0
  59. {dlms_spodes_client-0.19.22.dist-info → dlms_spodes_client-0.19.23.dist-info}/entry_points.txt +0 -0
  60. {dlms_spodes_client-0.19.22.dist-info → dlms_spodes_client-0.19.23.dist-info}/top_level.txt +0 -0
@@ -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