DLMS-SPODES-client 0.19.35__py3-none-any.whl → 0.19.37__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- DLMS_SPODES_client/FCS16.py +39 -39
- DLMS_SPODES_client/__init__.py +12 -12
- DLMS_SPODES_client/client.py +2093 -2093
- DLMS_SPODES_client/gurux_common/enums/TraceLevel.py +21 -21
- DLMS_SPODES_client/gurux_dlms/AesGcmParameter.py +37 -37
- DLMS_SPODES_client/gurux_dlms/CountType.py +16 -16
- DLMS_SPODES_client/gurux_dlms/GXByteBuffer.py +545 -545
- DLMS_SPODES_client/gurux_dlms/GXCiphering.py +196 -196
- DLMS_SPODES_client/gurux_dlms/GXDLMS.py +426 -426
- DLMS_SPODES_client/gurux_dlms/GXDLMSChippering.py +237 -237
- DLMS_SPODES_client/gurux_dlms/GXDLMSChipperingStream.py +977 -977
- DLMS_SPODES_client/gurux_dlms/GXDLMSConfirmedServiceError.py +90 -90
- DLMS_SPODES_client/gurux_dlms/GXDLMSException.py +139 -139
- DLMS_SPODES_client/gurux_dlms/GXDLMSLNParameters.py +33 -33
- DLMS_SPODES_client/gurux_dlms/GXDLMSSNParameters.py +21 -21
- DLMS_SPODES_client/gurux_dlms/GXDLMSSettings.py +254 -254
- DLMS_SPODES_client/gurux_dlms/GXReplyData.py +87 -87
- DLMS_SPODES_client/gurux_dlms/HdlcControlFrame.py +9 -9
- DLMS_SPODES_client/gurux_dlms/MBusCommand.py +8 -8
- DLMS_SPODES_client/gurux_dlms/MBusEncryptionMode.py +27 -27
- DLMS_SPODES_client/gurux_dlms/ResponseType.py +8 -8
- DLMS_SPODES_client/gurux_dlms/SetResponseType.py +29 -29
- DLMS_SPODES_client/gurux_dlms/_HDLCInfo.py +9 -9
- DLMS_SPODES_client/gurux_dlms/__init__.py +75 -75
- DLMS_SPODES_client/gurux_dlms/enums/Access.py +12 -12
- DLMS_SPODES_client/gurux_dlms/enums/ApplicationReference.py +14 -14
- DLMS_SPODES_client/gurux_dlms/enums/Authentication.py +41 -41
- DLMS_SPODES_client/gurux_dlms/enums/BerType.py +35 -35
- DLMS_SPODES_client/gurux_dlms/enums/Command.py +285 -285
- DLMS_SPODES_client/gurux_dlms/enums/Definition.py +9 -9
- DLMS_SPODES_client/gurux_dlms/enums/ErrorCode.py +46 -46
- DLMS_SPODES_client/gurux_dlms/enums/ExceptionServiceError.py +12 -12
- DLMS_SPODES_client/gurux_dlms/enums/HardwareResource.py +10 -10
- DLMS_SPODES_client/gurux_dlms/enums/HdlcFrameType.py +9 -9
- DLMS_SPODES_client/gurux_dlms/enums/Initiate.py +10 -10
- DLMS_SPODES_client/gurux_dlms/enums/LoadDataSet.py +13 -13
- DLMS_SPODES_client/gurux_dlms/enums/ObjectType.py +306 -306
- DLMS_SPODES_client/gurux_dlms/enums/Priority.py +7 -7
- DLMS_SPODES_client/gurux_dlms/enums/RequestTypes.py +9 -9
- DLMS_SPODES_client/gurux_dlms/enums/Security.py +14 -14
- DLMS_SPODES_client/gurux_dlms/enums/Service.py +16 -16
- DLMS_SPODES_client/gurux_dlms/enums/ServiceClass.py +9 -9
- DLMS_SPODES_client/gurux_dlms/enums/ServiceError.py +8 -8
- DLMS_SPODES_client/gurux_dlms/enums/Standard.py +18 -18
- DLMS_SPODES_client/gurux_dlms/enums/StateError.py +7 -7
- DLMS_SPODES_client/gurux_dlms/enums/Task.py +10 -10
- DLMS_SPODES_client/gurux_dlms/enums/VdeStateError.py +10 -10
- DLMS_SPODES_client/gurux_dlms/enums/__init__.py +33 -33
- DLMS_SPODES_client/gurux_dlms/internal/_GXCommon.py +1673 -1673
- DLMS_SPODES_client/logger.py +56 -56
- DLMS_SPODES_client/services.py +90 -90
- DLMS_SPODES_client/session.py +363 -363
- DLMS_SPODES_client/settings.py +48 -48
- DLMS_SPODES_client/task.py +1884 -1884
- {dlms_spodes_client-0.19.35.dist-info → dlms_spodes_client-0.19.37.dist-info}/METADATA +29 -29
- dlms_spodes_client-0.19.37.dist-info/RECORD +61 -0
- {dlms_spodes_client-0.19.35.dist-info → dlms_spodes_client-0.19.37.dist-info}/WHEEL +1 -1
- dlms_spodes_client-0.19.35.dist-info/RECORD +0 -61
- {dlms_spodes_client-0.19.35.dist-info → dlms_spodes_client-0.19.37.dist-info}/entry_points.txt +0 -0
- {dlms_spodes_client-0.19.35.dist-info → dlms_spodes_client-0.19.37.dist-info}/top_level.txt +0 -0
DLMS_SPODES_client/task.py
CHANGED
|
@@ -1,1884 +1,1884 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import random
|
|
3
|
-
from copy import copy
|
|
4
|
-
import re
|
|
5
|
-
from typing_extensions import deprecated
|
|
6
|
-
import hashlib
|
|
7
|
-
from dataclasses import dataclass, field
|
|
8
|
-
from typing import Callable, Any, Optional, Protocol, cast, override, Self, Final, Iterable, TypeVarTuple
|
|
9
|
-
from itertools import count
|
|
10
|
-
import datetime
|
|
11
|
-
import time
|
|
12
|
-
from semver import Version as SemVer
|
|
13
|
-
from StructResult import result
|
|
14
|
-
from DLMS_SPODES.pardata import ParValues
|
|
15
|
-
from DLMS_SPODES.types.implementations import enums, long_unsigneds, bitstrings, octet_string, structs, arrays, integers
|
|
16
|
-
from DLMS_SPODES.pardata import ParData
|
|
17
|
-
from DLMS_SPODES.cosem_interface_classes import parameters as dlms_par
|
|
18
|
-
from DLMS_SPODES.cosem_interface_classes.parameter import Parameter
|
|
19
|
-
from DLMS_SPODES import exceptions as exc, pdu_enums as pdu
|
|
20
|
-
from DLMS_SPODES.cosem_interface_classes import (
|
|
21
|
-
cosem_interface_class as ic,
|
|
22
|
-
collection,
|
|
23
|
-
overview,
|
|
24
|
-
ln_pattern
|
|
25
|
-
)
|
|
26
|
-
from DLMS_SPODES.cosem_interface_classes.clock import Clock
|
|
27
|
-
from DLMS_SPODES.cosem_interface_classes.image_transfer.image_transfer_status import ImageTransferStatus
|
|
28
|
-
from DLMS_SPODES.cosem_interface_classes.image_transfer.ver0 import ImageTransferInitiate, ImageBlockTransfer, ImageToActivateInfo
|
|
29
|
-
from DLMS_SPODES.cosem_interface_classes.association_ln.ver0 import ObjectListType, ObjectListElement
|
|
30
|
-
from DLMS_SPODES.cosem_interface_classes import association_ln
|
|
31
|
-
from DLMS_SPODES.types import cdt, ut, cst
|
|
32
|
-
from DLMS_SPODES.hdlc import frame
|
|
33
|
-
from DLMS_SPODES.enums import Transmit, Application, Conformance
|
|
34
|
-
from DLMS_SPODES.firmwares import get_firmware
|
|
35
|
-
from DLMS_SPODES.cosem_interface_classes.image_transfer import image_transfer_status as i_t_status
|
|
36
|
-
from DLMSAdapter.main import AdapterException, Adapter, gag
|
|
37
|
-
from DLMSCommunicationProfile.osi import OSI
|
|
38
|
-
from .logger import LogLevel as logL
|
|
39
|
-
from .client import Client, Security, Data, mechanism_id, AcseServiceUser, State
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
firm_id_pat = re.compile(b".*(?P<fid>PWRM_M2M_[^_]{1,10}_[^_]{1,10}).+")
|
|
43
|
-
boot_ver_pat = re.compile(b"(?P<boot_ver>\\d{1,4}).+")
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
type Errors = list[Exception]
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
class Base[T: result.Result](Protocol):
|
|
50
|
-
"""Exchange task for DLMS client"""
|
|
51
|
-
msg: str
|
|
52
|
-
|
|
53
|
-
def copy(self) -> Self: ...
|
|
54
|
-
|
|
55
|
-
@property
|
|
56
|
-
def current(self) -> 'Base[T] | Self':
|
|
57
|
-
return self
|
|
58
|
-
|
|
59
|
-
async def run(self, c: Client) -> T | result.Error:
|
|
60
|
-
"""exception handling block"""
|
|
61
|
-
try:
|
|
62
|
-
return await self.physical(c)
|
|
63
|
-
except (ConnectionRefusedError, TimeoutError) as e:
|
|
64
|
-
return result.Error.from_e(e)
|
|
65
|
-
except exc.DLMSException as e:
|
|
66
|
-
return result.Error.from_e(e)
|
|
67
|
-
except Exception as e:
|
|
68
|
-
return result.Error.from_e(e)
|
|
69
|
-
# except asyncio.CancelledError as e:
|
|
70
|
-
# await c.close() # todo: change to DiscRequest
|
|
71
|
-
# return result.Error.from_e(exc.Abort("manual stop")) # for handle BaseException
|
|
72
|
-
finally:
|
|
73
|
-
c.received_frames.clear() # for next exchange need clear all received frames. todo: this bag, remove in future
|
|
74
|
-
|
|
75
|
-
async def PH_connect(self, c: Client) -> result.Ok | result.Error:
|
|
76
|
-
if c.media is None:
|
|
77
|
-
return result.Error.from_e(exc.NoPort("no media"), "PH_connect")
|
|
78
|
-
if not c.media.is_open():
|
|
79
|
-
if isinstance(res_open := await c.media.open(), result.Error):
|
|
80
|
-
return res_open
|
|
81
|
-
c.log(logL.INFO, F"Open port communication channel: {c.media} {res_open.value}sec")
|
|
82
|
-
c.level = OSI.PHYSICAL
|
|
83
|
-
# todo: replace to <data_link>
|
|
84
|
-
if (
|
|
85
|
-
c._objects is None
|
|
86
|
-
and not isinstance(self, InitType)
|
|
87
|
-
):
|
|
88
|
-
if isinstance(res := await init_type.data_link(c), result.Error):
|
|
89
|
-
return res.with_msg("PH_connect")
|
|
90
|
-
if isinstance(res_close := await c.close(), result.Error): # todo: change to DiscRequest, or make not closed or reconnect !!!
|
|
91
|
-
return res_close
|
|
92
|
-
return result.OK
|
|
93
|
-
|
|
94
|
-
@staticmethod
|
|
95
|
-
async def physical_t(c: Client) -> result.Ok | result.Error:
|
|
96
|
-
return await c.close()
|
|
97
|
-
|
|
98
|
-
async def physical(self, c: Client) -> T | result.Error:
|
|
99
|
-
if OSI.PHYSICAL not in c.level:
|
|
100
|
-
if isinstance((res := await self.PH_connect(c)), result.Error):
|
|
101
|
-
return res
|
|
102
|
-
ret = await self.data_link(c)
|
|
103
|
-
if isinstance(res_terminate := await self.physical_t(c), result.Error):
|
|
104
|
-
return res_terminate
|
|
105
|
-
return ret
|
|
106
|
-
|
|
107
|
-
async def DL_connect(self, c: Client) -> result.Ok | result.Error:
|
|
108
|
-
"""Data link Layer connect"""
|
|
109
|
-
c.send_frames.clear()
|
|
110
|
-
# calculate addresses todo: move to c.com_profile(HDLC)
|
|
111
|
-
c.DA = frame.Address(
|
|
112
|
-
upper_address=int(c.server_SAP),
|
|
113
|
-
lower_address=c.com_profile.parameters.device_address,
|
|
114
|
-
length=c.addr_size
|
|
115
|
-
)
|
|
116
|
-
c.SA = frame.Address(upper_address=int(c.SAP))
|
|
117
|
-
c.log(logL.INFO, F"{c.SA=} {c.DA=}")
|
|
118
|
-
# initialize connection
|
|
119
|
-
if c.settings.cipher.security != Security.NONE:
|
|
120
|
-
c.log(logL.DEB, F"Security: {c.settings.cipher.security}/n"
|
|
121
|
-
F"System title: {c.settings.cipher.systemTitle.hex()}"
|
|
122
|
-
F"Authentication key: {c.settings.cipher.authenticationKey.hex()}"
|
|
123
|
-
F"Block cipher key: {c.settings.cipher.blockCipherKey.hex()}")
|
|
124
|
-
if c.settings.cipher.dedicatedKey:
|
|
125
|
-
c.log(logL.DEB, F"Dedicated key: {c.settings.cipher.dedicatedKey.hex()}")
|
|
126
|
-
# SNRM
|
|
127
|
-
c.get_SNRM_request()
|
|
128
|
-
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
129
|
-
return res_pdu
|
|
130
|
-
c.level |= OSI.DATA_LINK
|
|
131
|
-
return result.OK
|
|
132
|
-
|
|
133
|
-
async def data_link(self, c: Client) -> T | result.Error:
|
|
134
|
-
if OSI.DATA_LINK not in c.level:
|
|
135
|
-
if isinstance(res_conn := await self.DL_connect(c), result.Error):
|
|
136
|
-
return res_conn
|
|
137
|
-
# todo: make tile
|
|
138
|
-
return await self.application(c)
|
|
139
|
-
|
|
140
|
-
async def AA(self, c: Client) -> result.Ok | result.Error:
|
|
141
|
-
"""Application Associate"""
|
|
142
|
-
if c.invocationCounter and c.settings.cipher is not None and c.settings.cipher.security != Security.NONE:
|
|
143
|
-
# create IC object. TODO: remove it after close connection, maybe???
|
|
144
|
-
c.settings.proposedConformance |= Conformance.GENERAL_PROTECTION
|
|
145
|
-
|
|
146
|
-
# my block
|
|
147
|
-
IC: Data = c.objects.add_if_missing(ut.CosemClassId(1),
|
|
148
|
-
logical_name=cst.LogicalName(bytearray((0, c.get_channel_index(), 43, 1,
|
|
149
|
-
c.current_association.security_setup_reference.e, 255))),
|
|
150
|
-
version=cdt.Unsigned(0))
|
|
151
|
-
tmp_client_SAP = c.current_association.associated_partners_id.client_SAP
|
|
152
|
-
challenge = c.settings.ctoSChallenge
|
|
153
|
-
try:
|
|
154
|
-
c.aarqRequest(c.m_id)
|
|
155
|
-
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
156
|
-
return res_pdu
|
|
157
|
-
ret = c.parseAareResponse(res_pdu.value)
|
|
158
|
-
c.level |= OSI.APPLICATION # todo: it's must be result of <ret> look down
|
|
159
|
-
if isinstance(res_ic := await ReadObjAttr(IC, 2).exchange(c), result.Error):
|
|
160
|
-
return res_ic
|
|
161
|
-
c.settings.cipher.invocationCounter = 1 + int(res_ic.value)
|
|
162
|
-
c.log(logL.DEB, "Invocation counter: " + str(c.settings.cipher.invocationCounter))
|
|
163
|
-
# disconnect
|
|
164
|
-
if c.media and c.media.is_open():
|
|
165
|
-
c.log(logL.DEB, "DisconnectRequest")
|
|
166
|
-
if isinstance(res_disc_req := await c.disconnect_request(), result.Error):
|
|
167
|
-
return res_disc_req
|
|
168
|
-
finally:
|
|
169
|
-
c.SAP = tmp_client_SAP
|
|
170
|
-
c.settings.useCustomChallenge = challenge is not None
|
|
171
|
-
c.settings.ctoSChallenge = challenge
|
|
172
|
-
|
|
173
|
-
# gurux with several removed methods
|
|
174
|
-
# add = self.settings.clientAddress
|
|
175
|
-
# auth = self.settings.authentication
|
|
176
|
-
# security = self.client.ciphering.security
|
|
177
|
-
# challenge = self.client.ctoSChallenge
|
|
178
|
-
# try:
|
|
179
|
-
# self.client.clientAddress = 16
|
|
180
|
-
# self.settings.authentication = Authentication.NONE
|
|
181
|
-
# self.client.ciphering.security = Security.NONE
|
|
182
|
-
# reply = GXReplyData()
|
|
183
|
-
# self.get_SNRM_request()
|
|
184
|
-
# self.status = Status.READ
|
|
185
|
-
# self.read_data_block2()
|
|
186
|
-
# self.objects.IEC_HDLS_setup.set_from_info(self.reply.data.get_data())
|
|
187
|
-
# self.connection_state = ConnectionState.HDLC
|
|
188
|
-
# self.reply.clear()
|
|
189
|
-
# self.aarqRequest()
|
|
190
|
-
# self.read_data_block2()
|
|
191
|
-
# self.parseAareResponse(reply.data)
|
|
192
|
-
# reply.clear()
|
|
193
|
-
# item = GXDLMSData(self.invocationCounter)
|
|
194
|
-
# data = self.client.read(item, 2)[0]
|
|
195
|
-
# reply = GXReplyData()
|
|
196
|
-
# self.read_data_block(data, reply)
|
|
197
|
-
# item.encodings[2] = reply.data.get_data()
|
|
198
|
-
# Update data type on read.
|
|
199
|
-
# if item.getDataType(2) == cdt.NullData.TAG:
|
|
200
|
-
# item.setDataType(2, reply.valueType)
|
|
201
|
-
# self.client.updateValue(item, 2, reply.value)
|
|
202
|
-
# self.client.ciphering.invocationCounter = 1 + item.value
|
|
203
|
-
# print("Invocation counter: " + str(self.client.ciphering.invocationCounter))
|
|
204
|
-
# if self.media and self.media.isOpen():
|
|
205
|
-
# self.log(logL.INFO, "DisconnectRequest")
|
|
206
|
-
# self.disconnect_request()
|
|
207
|
-
# finally:
|
|
208
|
-
# self.settings.clientAddress = add
|
|
209
|
-
# self.settings.authentication = auth
|
|
210
|
-
# self.client.ciphering.security = security
|
|
211
|
-
# self.client.ctoSChallenge = challenge
|
|
212
|
-
|
|
213
|
-
c.aarqRequest(c.m_id)
|
|
214
|
-
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
215
|
-
return res_pdu
|
|
216
|
-
# await c.read_attr(ut.CosemAttributeDescriptor((collection.ClassID.ASSOCIATION_LN, ut.CosemObjectInstanceId("0.0.40.0.0.255"), ut.CosemObjectAttributeId(6)))) # for test only
|
|
217
|
-
try:
|
|
218
|
-
parse = c.parseAareResponse(res_pdu.value)
|
|
219
|
-
except IndexError as e:
|
|
220
|
-
print(e)
|
|
221
|
-
match parse:
|
|
222
|
-
case AcseServiceUser.NULL:
|
|
223
|
-
c.log(logL.INFO, "Authentication success")
|
|
224
|
-
c.level |= OSI.APPLICATION
|
|
225
|
-
case AcseServiceUser.AUTHENTICATION_REQUIRED:
|
|
226
|
-
c.getApplicationAssociationRequest()
|
|
227
|
-
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
228
|
-
return res_pdu
|
|
229
|
-
c.parseApplicationAssociationResponse(res_pdu.value)
|
|
230
|
-
case _ as diagnostic:
|
|
231
|
-
return result.Error.from_e(exc.AssociationResultError(diagnostic))
|
|
232
|
-
if c._objects is not None:
|
|
233
|
-
matchLDN = change_ldn if c.is_universal() else match_ldn
|
|
234
|
-
if isinstance(res_match_ldn := await matchLDN.exchange(c), result.Error):
|
|
235
|
-
return res_match_ldn
|
|
236
|
-
return result.OK
|
|
237
|
-
|
|
238
|
-
async def application(self, c: Client) -> T | result.Error:
|
|
239
|
-
if OSI.APPLICATION not in c.level:
|
|
240
|
-
if isinstance(res := await self.AA(c), result.Error):
|
|
241
|
-
return res
|
|
242
|
-
# no tile
|
|
243
|
-
return await self.exchange(c)
|
|
244
|
-
|
|
245
|
-
async def exchange(self, c: Client) -> T | result.Error:
|
|
246
|
-
"""application level exchange"""
|
|
247
|
-
|
|
248
|
-
async def connect(self, c: Client) -> result.Ok | result.Error:
|
|
249
|
-
await self.PH_connect(c)
|
|
250
|
-
await self.DL_connect(c)
|
|
251
|
-
return await self.AA(c)
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
class SimpleCopy:
|
|
255
|
-
def copy(self) -> Self:
|
|
256
|
-
return self
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
class Simple[T](Base[result.Simple[T]], Protocol):
|
|
260
|
-
"""Simple result"""
|
|
261
|
-
@override
|
|
262
|
-
async def exchange(self, c: Client) -> result.SimpleOrError[T]: ...
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
class Boolean(Simple[bool], Protocol):
|
|
266
|
-
"""Simple[bool] result"""
|
|
267
|
-
@override
|
|
268
|
-
async def exchange(self, c: Client) -> result.SimpleOrError[bool]: ...
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
class CDT[T: cdt.CommonDataType](Simple[T], Protocol):
|
|
272
|
-
"""Simple[CDT] result"""
|
|
273
|
-
@override
|
|
274
|
-
async def exchange(self, c: Client) -> result.SimpleOrError[T]: ...
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
class _List[T](Base[result.List[T]], Protocol):
|
|
278
|
-
"""With List result"""
|
|
279
|
-
@override
|
|
280
|
-
async def exchange(self, c: Client) -> result.List[T] | result.Error: ...
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
class _Sequence[*Ts](Base[result.Sequence[*Ts]], Protocol):
|
|
284
|
-
"""With List result"""
|
|
285
|
-
@override
|
|
286
|
-
async def exchange(self, c: Client) -> result.Sequence[*Ts] | result.Error: ...
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
class OK(Base[result.Ok], Protocol):
|
|
290
|
-
"""Always result OK"""
|
|
291
|
-
|
|
292
|
-
@override
|
|
293
|
-
async def exchange(self, c: Client) -> result.Ok | result.Error: ...
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
class StrictOK(Base[result.StrictOk], Protocol):
|
|
297
|
-
"""Always result OK"""
|
|
298
|
-
|
|
299
|
-
@override
|
|
300
|
-
async def exchange(self, c: Client) -> result.StrictOk | result.Error: ...
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
@dataclass(frozen=True)
|
|
304
|
-
class ClientBlocking(SimpleCopy, OK):
|
|
305
|
-
"""complete by time or abort"""
|
|
306
|
-
delay: float = field(default=99999999.0)
|
|
307
|
-
msg: str = "client blocking"
|
|
308
|
-
|
|
309
|
-
async def run(self, c: Client) -> result.Ok | result.Error:
|
|
310
|
-
try:
|
|
311
|
-
c.level = OSI.APPLICATION
|
|
312
|
-
c.log(logL.WARN, F"blocked for {self.delay} second")
|
|
313
|
-
await asyncio.sleep(self.delay)
|
|
314
|
-
return result.OK
|
|
315
|
-
finally:
|
|
316
|
-
c.level = OSI.NONE
|
|
317
|
-
return result.OK
|
|
318
|
-
|
|
319
|
-
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
320
|
-
raise RuntimeError(f"not support for {self.__class__.__name__}")
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
# todo: make with <data_link>
|
|
324
|
-
@dataclass
|
|
325
|
-
class TestDataLink(SimpleCopy, OK):
|
|
326
|
-
msg: str = "test DLink"
|
|
327
|
-
|
|
328
|
-
async def physical(self, c: Client) -> result.Ok | result.Error:
|
|
329
|
-
if OSI.PHYSICAL not in c.level:
|
|
330
|
-
if not c.media.is_open():
|
|
331
|
-
if isinstance(res_open := await c.media.open(), result.Error):
|
|
332
|
-
return res_open
|
|
333
|
-
c.level = OSI.PHYSICAL
|
|
334
|
-
c.DA = frame.Address(
|
|
335
|
-
upper_address=int(c.server_SAP),
|
|
336
|
-
lower_address=c.com_profile.parameters.device_address,
|
|
337
|
-
length=c.addr_size
|
|
338
|
-
)
|
|
339
|
-
c.SA = frame.Address(upper_address=int(c.SAP))
|
|
340
|
-
c.get_SNRM_request()
|
|
341
|
-
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
342
|
-
return res_pdu
|
|
343
|
-
c.level |= OSI.DATA_LINK
|
|
344
|
-
if isinstance(res_close := await c.close(), result.Error): # todo: change to DiscRequest
|
|
345
|
-
return res_close
|
|
346
|
-
return result.Ok
|
|
347
|
-
|
|
348
|
-
async def exchange(self, c: Client):
|
|
349
|
-
return result.OK
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
@dataclass(frozen=True)
|
|
353
|
-
class Dummy(SimpleCopy, OK):
|
|
354
|
-
msg: str = "dummy"
|
|
355
|
-
|
|
356
|
-
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
357
|
-
""""""
|
|
358
|
-
return result.OK
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
@dataclass(frozen=True)
|
|
362
|
-
class HardwareDisconnect(SimpleCopy, OK):
|
|
363
|
-
msg: str = "hardware disconnect"
|
|
364
|
-
|
|
365
|
-
@override
|
|
366
|
-
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
367
|
-
await c.media.close()
|
|
368
|
-
c.level = OSI.NONE
|
|
369
|
-
msg = '' if self.msg is None else F": {self.msg}"
|
|
370
|
-
c.log(logL.WARN, F"HARDWARE DISCONNECT{msg}")
|
|
371
|
-
return result.OK
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
@dataclass(frozen=True)
|
|
375
|
-
class HardwareReconnect(SimpleCopy, OK):
|
|
376
|
-
delay: float = 0.0
|
|
377
|
-
"""delay between disconnect and restore Application"""
|
|
378
|
-
msg: str = "reconnect media without response"
|
|
379
|
-
|
|
380
|
-
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
381
|
-
if isinstance((res := await HardwareDisconnect().exchange(c)), result.Error):
|
|
382
|
-
return res
|
|
383
|
-
if self.delay != 0.0:
|
|
384
|
-
c.log(logL.INFO, F"delay({self.delay})")
|
|
385
|
-
await asyncio.sleep(self.delay)
|
|
386
|
-
if isinstance(res_connect := await self.connect(c), result.Error): # restore Application
|
|
387
|
-
return res_connect
|
|
388
|
-
return result.OK
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
@dataclass(frozen=True)
|
|
392
|
-
class Loop(OK):
|
|
393
|
-
task: Base[result.Result]
|
|
394
|
-
func: Callable[[Any], bool]
|
|
395
|
-
delay: int = 0.0
|
|
396
|
-
msg: str = "loop"
|
|
397
|
-
attempt_amount: int = 0
|
|
398
|
-
"""0 is never end loop"""
|
|
399
|
-
|
|
400
|
-
def copy(self) -> Self:
|
|
401
|
-
return Loop(
|
|
402
|
-
task=self.task.copy(),
|
|
403
|
-
func=self.func,
|
|
404
|
-
delay=self.delay,
|
|
405
|
-
msg=self.msg,
|
|
406
|
-
attempt_amount=self.attempt_amount
|
|
407
|
-
)
|
|
408
|
-
|
|
409
|
-
async def run(self, c: Client) -> result.Result:
|
|
410
|
-
attempt = count()
|
|
411
|
-
while not self.func(await super(Loop, self).run(c)):
|
|
412
|
-
if next(attempt) == self.attempt_amount:
|
|
413
|
-
return result.Error.from_e(ValueError("end of attempts"))
|
|
414
|
-
await asyncio.sleep(self.delay)
|
|
415
|
-
return result.OK
|
|
416
|
-
|
|
417
|
-
async def exchange(self, c: Client) -> result.Result:
|
|
418
|
-
return await self.task.exchange(c)
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
@dataclass
|
|
422
|
-
class ConditionalTask[T](Base):
|
|
423
|
-
"""Warning: experimental"""
|
|
424
|
-
precondition_task: Base[result.Result]
|
|
425
|
-
comp_value: T
|
|
426
|
-
predicate: Callable[[T, T], bool]
|
|
427
|
-
main_task: Base[result.Result]
|
|
428
|
-
msg: str = "conditional task"
|
|
429
|
-
|
|
430
|
-
async def exchange(self, c: Client) -> result.Result:
|
|
431
|
-
res = await self.precondition_task.exchange(c)
|
|
432
|
-
if self.predicate(self.comp_value, res.value):
|
|
433
|
-
return await self.main_task.exchange(c)
|
|
434
|
-
return result.Error.from_e(ValueError("Condition not satisfied"))
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
DEFAULT_DATETIME_SCHEDULER = cdt.DateTime.parse("1.1.0001")
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
@dataclass
|
|
441
|
-
class Scheduler[T: result.Result](Base[T]):
|
|
442
|
-
"""𝑟𝑒𝑝𝑒𝑡𝑖𝑡𝑖𝑜𝑛_𝑑𝑒𝑙𝑎𝑦 = 𝑟𝑒𝑝𝑒𝑡𝑖𝑡𝑖𝑜𝑛_𝑑𝑒𝑙𝑎𝑦_𝑚𝑖𝑛 × (𝑟𝑒𝑝𝑒𝑡𝑖𝑡𝑖𝑜𝑛_𝑑𝑒𝑙𝑎𝑦_𝑒𝑥𝑝𝑜𝑛𝑒𝑛𝑡 × 0.01) ** 𝑛"""
|
|
443
|
-
task: Base[T]
|
|
444
|
-
execution_datetime: Final[cdt.DateTime] = DEFAULT_DATETIME_SCHEDULER
|
|
445
|
-
start_interval: Final[int] = 0
|
|
446
|
-
number_of_retries: Final[int] = 3
|
|
447
|
-
total_of_retries: Final[int] = 100
|
|
448
|
-
repetition_delay_min: Final[int] = 1
|
|
449
|
-
repetition_delay_exponent: Final[int] = 100
|
|
450
|
-
repetition_delay_max: Final[int] = 100
|
|
451
|
-
msg: str = "sheduler"
|
|
452
|
-
|
|
453
|
-
def copy(self) -> "Scheduler[T]":
|
|
454
|
-
return Scheduler(
|
|
455
|
-
task=self.task.copy(),
|
|
456
|
-
execution_datetime=self.execution_datetime,
|
|
457
|
-
start_interval=self.start_interval,
|
|
458
|
-
number_of_retries=self.number_of_retries,
|
|
459
|
-
total_of_retries=self.total_of_retries,
|
|
460
|
-
repetition_delay_min=self.repetition_delay_min,
|
|
461
|
-
repetition_delay_exponent=self.repetition_delay_exponent,
|
|
462
|
-
repetition_delay_max=self.repetition_delay_max,
|
|
463
|
-
msg=self.msg
|
|
464
|
-
)
|
|
465
|
-
|
|
466
|
-
async def run(self, c: Client) -> T | result.Error:
|
|
467
|
-
if self.start_interval != 0:
|
|
468
|
-
await asyncio.sleep(random.uniform(0, self.start_interval))
|
|
469
|
-
c.log(logL.INFO, f"start {self.__class__.__name__}")
|
|
470
|
-
total_of_retries = count()
|
|
471
|
-
is_start: bool = True
|
|
472
|
-
acc = result.ErrorAccumulator()
|
|
473
|
-
while True:
|
|
474
|
-
dt = self.execution_datetime.get_right_nearest_datetime(now := datetime.datetime.now())
|
|
475
|
-
if dt is None:
|
|
476
|
-
if is_start:
|
|
477
|
-
is_start = False
|
|
478
|
-
else:
|
|
479
|
-
return acc.as_error(exc.Timeout("start time is out"), msg=self.msg)
|
|
480
|
-
else:
|
|
481
|
-
delay = (dt - now).total_seconds()
|
|
482
|
-
c.log(logL.WARN, f"wait for {delay=}")
|
|
483
|
-
await asyncio.sleep(delay)
|
|
484
|
-
for n in range(self.number_of_retries):
|
|
485
|
-
if next(total_of_retries) > self.total_of_retries:
|
|
486
|
-
return acc.as_error(exc.Timeout("out of total retries"), msg=self.msg)
|
|
487
|
-
await asyncio.sleep(min(self.repetition_delay_max, self.repetition_delay_min*(self.repetition_delay_exponent * 0.01)**n))
|
|
488
|
-
if isinstance(res := await super(Scheduler, self).run(c), result.Error):
|
|
489
|
-
acc.append_err(res.err)
|
|
490
|
-
else:
|
|
491
|
-
return res
|
|
492
|
-
|
|
493
|
-
async def exchange(self, c: Client) -> T | result.Error:
|
|
494
|
-
return await self.task.exchange(c)
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
@dataclass
|
|
498
|
-
class Subtasks[U: Base[result.Result]](Protocol):
|
|
499
|
-
"""for register longer other tasks into task"""
|
|
500
|
-
tasks: Iterable[U]
|
|
501
|
-
|
|
502
|
-
@property
|
|
503
|
-
def current(self) -> U | Self: ...
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
class List[T: result.Result, U: Base[result.Result]](Subtasks[U], _List[T]):
|
|
507
|
-
"""for exchange task sequence"""
|
|
508
|
-
__is_exchange: bool
|
|
509
|
-
err_ignore: bool
|
|
510
|
-
msg: str
|
|
511
|
-
__current: Base[T]
|
|
512
|
-
|
|
513
|
-
def __init__(self, *tasks: Base[T], msg: str = "", err_ignore: bool = False):
|
|
514
|
-
self.tasks = list(tasks)
|
|
515
|
-
self.__current = self
|
|
516
|
-
self.__is_exchange = False
|
|
517
|
-
self.msg = self.__class__.__name__ if msg == "" else msg
|
|
518
|
-
self.err_ignore = err_ignore
|
|
519
|
-
|
|
520
|
-
def copy(self) -> Self:
|
|
521
|
-
if all((isinstance(t, SimpleCopy) for t in self.tasks)):
|
|
522
|
-
return self
|
|
523
|
-
return List(
|
|
524
|
-
*(t.copy() for t in self.tasks),
|
|
525
|
-
msg=self.msg,
|
|
526
|
-
err_ignore=self.err_ignore
|
|
527
|
-
)
|
|
528
|
-
|
|
529
|
-
@property
|
|
530
|
-
def current(self) -> 'Base[T] | Self':
|
|
531
|
-
return self.__current
|
|
532
|
-
|
|
533
|
-
def append(self, task: Base[T]):
|
|
534
|
-
if not self.__is_exchange:
|
|
535
|
-
self.tasks.append(task)
|
|
536
|
-
else:
|
|
537
|
-
raise RuntimeError(F"append to {self.__class__.__name__} not allowed, already exchange started")
|
|
538
|
-
|
|
539
|
-
async def exchange(self, c: Client) -> result.List[T] | result.Error:
|
|
540
|
-
res = result.List()
|
|
541
|
-
self.__is_exchange = True
|
|
542
|
-
for t in self.tasks:
|
|
543
|
-
self.__current = t
|
|
544
|
-
if (
|
|
545
|
-
isinstance(res_one := await t.exchange(c), result.Error)
|
|
546
|
-
and not self.err_ignore
|
|
547
|
-
):
|
|
548
|
-
return res_one
|
|
549
|
-
res.append(res_one)
|
|
550
|
-
return res
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
class Sequence[*Ts](Subtasks[Base[result.Result]], _Sequence[*Ts]):
|
|
554
|
-
"""for exchange task sequence"""
|
|
555
|
-
msg: str
|
|
556
|
-
err_ignore: bool
|
|
557
|
-
__current: "Base[result.Result] | Sequence[*Ts]"
|
|
558
|
-
tasks: tuple[Base[result.Result], ...]
|
|
559
|
-
|
|
560
|
-
def __init__(self, *tasks: Base[result.Result], msg: str = "sequence", err_ignore: bool = False):
|
|
561
|
-
self.tasks = tasks
|
|
562
|
-
self.__current = self
|
|
563
|
-
self.msg = self.__class__.__name__ if msg == "" else msg
|
|
564
|
-
self.err_ignore = err_ignore
|
|
565
|
-
|
|
566
|
-
def copy(self) -> "Sequence[*Ts]":
|
|
567
|
-
if all((isinstance(t, SimpleCopy) for t in self.tasks)):
|
|
568
|
-
return self
|
|
569
|
-
return Sequence(
|
|
570
|
-
*(t.copy() for t in self.tasks),
|
|
571
|
-
msg=self.msg,
|
|
572
|
-
err_ignore=self.err_ignore
|
|
573
|
-
)
|
|
574
|
-
|
|
575
|
-
@property
|
|
576
|
-
def current(self) -> Base[result.Result] | Self:
|
|
577
|
-
return self.__current
|
|
578
|
-
|
|
579
|
-
async def exchange(self, c: Client) -> result.Sequence[*Ts] | result.Error:
|
|
580
|
-
res = result.Sequence()
|
|
581
|
-
for t in self.tasks:
|
|
582
|
-
self.__current = t
|
|
583
|
-
if isinstance(res_one := await t.exchange(c), result.Error):
|
|
584
|
-
if self.err_ignore:
|
|
585
|
-
res_one = res_one.with_msg(self.msg)
|
|
586
|
-
else:
|
|
587
|
-
return res_one
|
|
588
|
-
res = res.add(res_one)
|
|
589
|
-
return cast("result.Sequence[*Ts]", res)
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
@dataclass(frozen=True)
|
|
593
|
-
class SetLocalTime(SimpleCopy, OK):
|
|
594
|
-
"""without decide time transfer"""
|
|
595
|
-
msg: str = "set local time"
|
|
596
|
-
|
|
597
|
-
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
598
|
-
clock_obj: Clock = c.objects.get_object("0.0.1.0.0.255")
|
|
599
|
-
if isinstance(res := await ReadAttribute(
|
|
600
|
-
ln=clock_obj.logical_name,
|
|
601
|
-
index=3
|
|
602
|
-
).exchange(c), result.Error):
|
|
603
|
-
return ret
|
|
604
|
-
delta = datetime.timedelta(minutes=int(res.value))
|
|
605
|
-
dt = cst.OctetStringDateTime(datetime.datetime.now(datetime.UTC)+delta)
|
|
606
|
-
if isinstance(res := await WriteAttribute(
|
|
607
|
-
ln=clock_obj.logical_name,
|
|
608
|
-
index=2,
|
|
609
|
-
value=dt.encoding
|
|
610
|
-
).exchange(c), result.Error):
|
|
611
|
-
return res
|
|
612
|
-
return result.OK
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
@dataclass(frozen=True)
|
|
616
|
-
class GetFirmwareVersion(SimpleCopy, CDT):
|
|
617
|
-
msg: str = "get firmware version"
|
|
618
|
-
|
|
619
|
-
async def exchange(self, c: Client) -> result.SimpleOrError[cdt.CommonDataType]:
|
|
620
|
-
return await Par2Data(Parameter(c.objects.id.f_ver.par[:6]).get_attr(c.objects.id.f_ver.par[6])).exchange(c)
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
@dataclass(frozen=True)
|
|
624
|
-
class ReadByDescriptor(SimpleCopy, Simple[bytes]):
|
|
625
|
-
desc: ut.CosemMethodDescriptor
|
|
626
|
-
msg: str = "get encoding by Cosem-Attribute-Descriptor"
|
|
627
|
-
|
|
628
|
-
async def exchange(self, c: Client) -> result.SimpleOrError[bytes]:
|
|
629
|
-
c.get_get_request_normal(self.desc)
|
|
630
|
-
return await c.read_data_block()
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
@dataclass(frozen=True)
|
|
634
|
-
class FindFirmwareVersion(SimpleCopy, Simple[collection.ParameterValue]):
|
|
635
|
-
msg: str = "try find COSEM server version, return: instance(B group), CDT"
|
|
636
|
-
|
|
637
|
-
async def exchange(self, c: Client) -> result.SimpleOrError[collection.ParameterValue]:
|
|
638
|
-
err = result.ErrorAccumulator()
|
|
639
|
-
for desc in (ut.CosemAttributeDescriptor((1, "0.0.0.2.1.255", 2)), ut.CosemAttributeDescriptor((1, "0.0.96.1.2.255", 2))):
|
|
640
|
-
if isinstance(res_read := await ReadByDescriptor(desc).exchange(c), result.Error):
|
|
641
|
-
err.append_err(res_read.err)
|
|
642
|
-
in_e, out_e = res_read.err.split(exc.ResultError)
|
|
643
|
-
if (
|
|
644
|
-
out_e is None
|
|
645
|
-
and in_e.exceptions[0].
|
|
646
|
-
):
|
|
647
|
-
continue
|
|
648
|
-
else:
|
|
649
|
-
return res_read
|
|
650
|
-
else:
|
|
651
|
-
res = result.Simple(collection.ParameterValue(
|
|
652
|
-
par=desc.instance_id.contents + desc.attribute_id.contents,
|
|
653
|
-
value=res_read.value
|
|
654
|
-
))
|
|
655
|
-
res.propagate_err(err)
|
|
656
|
-
return res
|
|
657
|
-
return err.as_error()
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
@dataclass(frozen=True)
|
|
661
|
-
class FindFirmwareId(SimpleCopy, Simple[collection.ParameterValue]):
|
|
662
|
-
msg: str = "find firmaware Identifier"
|
|
663
|
-
|
|
664
|
-
async def exchange(self, c: Client) -> result.SimpleOrError[collection.ParameterValue]:
|
|
665
|
-
err = result.ErrorAccumulator()
|
|
666
|
-
for desc in (ut.CosemAttributeDescriptor((1, "0.0.0.2.0.255", 2)), ut.CosemAttributeDescriptor((1, "0.0.96.1.1.255", 2))):
|
|
667
|
-
if isinstance(res_read := await ReadByDescriptor(desc).exchange(c), result.Error):
|
|
668
|
-
err.append_err(res_read.err)
|
|
669
|
-
in_e, out_e = res_read.err.split(exc.ResultError)
|
|
670
|
-
if (
|
|
671
|
-
out_e is None
|
|
672
|
-
and in_e.exceptions[0].
|
|
673
|
-
):
|
|
674
|
-
continue
|
|
675
|
-
else:
|
|
676
|
-
return res_read
|
|
677
|
-
else:
|
|
678
|
-
res = result.Simple(collection.ParameterValue(
|
|
679
|
-
par=desc.instance_id.contents + desc.attribute_id.contents,
|
|
680
|
-
value=res_read.value
|
|
681
|
-
))
|
|
682
|
-
res.propagate_err(err)
|
|
683
|
-
return res
|
|
684
|
-
return err.as_error()
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
@dataclass(frozen=True)
|
|
688
|
-
class KeepAlive(SimpleCopy, OK):
|
|
689
|
-
msg: str = "keep alive(read LND.ln)"
|
|
690
|
-
|
|
691
|
-
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
692
|
-
if isinstance(res := await Par2Data(dlms_par.LDN.value).exchange(c), result.Error):
|
|
693
|
-
return res
|
|
694
|
-
return result.OK
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
class GetLDN(SimpleCopy, CDT[octet_string.LDN]):
|
|
698
|
-
""":return LDN value"""
|
|
699
|
-
msg: str = "get LDN"
|
|
700
|
-
|
|
701
|
-
async def exchange(self, c: Client) -> result.SimpleOrError[octet_string.LDN]:
|
|
702
|
-
if isinstance(res := await ReadByDescriptor(collection.AttrDesc.LDN_VALUE).exchange(c), result.Error):
|
|
703
|
-
return res
|
|
704
|
-
return result.Simple(octet_string.LDN(res.value))
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
# todo: possible implementation ConditionalTask
|
|
708
|
-
# def compare_ldn(man: bytes, ldn: octet_string.LDN) -> bool:
|
|
709
|
-
# return man == ldn.get_manufacturer()
|
|
710
|
-
#
|
|
711
|
-
#
|
|
712
|
-
# check_LDN = ConditionalTask(
|
|
713
|
-
# precondition_task=GetLDN,
|
|
714
|
-
# comp_value=b"KPZ",
|
|
715
|
-
# predicate=compare_ldn,
|
|
716
|
-
# main_task=None
|
|
717
|
-
# )
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
@dataclass
|
|
721
|
-
class MatchLDN(OK):
|
|
722
|
-
universal: bool = field(default=False)
|
|
723
|
-
msg: str = "matching LDN"
|
|
724
|
-
|
|
725
|
-
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
726
|
-
if isinstance((res := await GetLDN().exchange(c)), result.Error):
|
|
727
|
-
return res.with_msg("match LDN")
|
|
728
|
-
if c.objects.LDN.value is None:
|
|
729
|
-
c._objects.LDN.set_attr(2, res.value)
|
|
730
|
-
elif c._objects.LDN.value == res.value:
|
|
731
|
-
"""secret matching"""
|
|
732
|
-
elif self.universal:
|
|
733
|
-
c.log(logL.WARN, F"connected to other server, change LDN")
|
|
734
|
-
await init_type.exchange(c) # todo: maybe set spec?
|
|
735
|
-
else:
|
|
736
|
-
return result.Error.from_e(ValueError(F"got LDN: {res.value}, expected {c._objects.LDN.value}"))
|
|
737
|
-
return result.OK
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
match_ldn = MatchLDN()
|
|
741
|
-
change_ldn = MatchLDN(universal=True)
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
@dataclass
|
|
745
|
-
class Lock:
|
|
746
|
-
__lock: asyncio.Lock = field(init=False, default_factory=asyncio.Lock)
|
|
747
|
-
piece: float = field(default=0.8)
|
|
748
|
-
name: str = ""
|
|
749
|
-
|
|
750
|
-
async def acquire(self, c: Client):
|
|
751
|
-
keep_alive = KeepAlive(self.name)
|
|
752
|
-
while True:
|
|
753
|
-
try:
|
|
754
|
-
await asyncio.wait_for(self.__lock.acquire(), c.com_profile.parameters.inactivity_time_out * self.piece) # todo: make not custom <inactivity_time_out>
|
|
755
|
-
return
|
|
756
|
-
except TimeoutError as e:
|
|
757
|
-
await keep_alive.exchange(c)
|
|
758
|
-
|
|
759
|
-
def release(self):
|
|
760
|
-
self.__lock.release()
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
@dataclass
|
|
764
|
-
class CreateType(SimpleCopy, Simple[collection.Collection]):
|
|
765
|
-
col_id: collection.ID
|
|
766
|
-
msg: str = "CreateType".__class__.__name__
|
|
767
|
-
obj_list: Optional[cdt.Array] = None
|
|
768
|
-
wait_list: asyncio.Lock = field(init=False)
|
|
769
|
-
"""wait <object list>"""
|
|
770
|
-
queue: asyncio.Queue = field(default_factory=asyncio.Queue)
|
|
771
|
-
col: collection.Collection = field(init=False)
|
|
772
|
-
|
|
773
|
-
def __post_init__(self):
|
|
774
|
-
self.col = collection.Collection(id_=self.col_id)
|
|
775
|
-
"""common collection"""
|
|
776
|
-
self.wait_list = Lock(name="wait <object list>")
|
|
777
|
-
|
|
778
|
-
async def exchange(self, c: Client) -> result.Simple[collection.Collection]:
|
|
779
|
-
res = result.Simple(self.col)
|
|
780
|
-
await self.wait_list.acquire(c)
|
|
781
|
-
try:
|
|
782
|
-
if self.obj_list is None:
|
|
783
|
-
if isinstance(res_obj_list := await ReadByDescriptor(collection.AttrDesc.OBJECT_LIST).exchange(c), result.Error):
|
|
784
|
-
return res_obj_list
|
|
785
|
-
self.obj_list = cdt.Array(res_obj_list.value, type_=structs.ObjectListElement)
|
|
786
|
-
if len(self.col) == 0:
|
|
787
|
-
for country_olel in filter(lambda it: ln_pattern.COUNTRY_SPECIFIC == it.logical_name, self.obj_list):
|
|
788
|
-
self.col.set_country(collection.CountrySpecificIdentifiers(country_olel.logical_name.d))
|
|
789
|
-
c.log(logL.INFO, F"set country: {self.col.country}")
|
|
790
|
-
match self.col.country:
|
|
791
|
-
case collection.CountrySpecificIdentifiers.RUSSIA:
|
|
792
|
-
country_desc = collection.AttrDesc.SPODES_VERSION
|
|
793
|
-
case _:
|
|
794
|
-
country_desc = None
|
|
795
|
-
if (
|
|
796
|
-
country_desc is not None
|
|
797
|
-
and next(filter(lambda it: country_desc.instance_id.contents == it.logical_name.contents, self.obj_list), False)
|
|
798
|
-
):
|
|
799
|
-
if isinstance(res_country_ver := await ReadByDescriptor(country_desc).exchange(c), result.Error):
|
|
800
|
-
return res_country_ver
|
|
801
|
-
self.col.set_country_ver(collection.ParameterValue(
|
|
802
|
-
par=country_desc.instance_id.contents + country_desc.attribute_id.contents,
|
|
803
|
-
value=res_country_ver.value
|
|
804
|
-
))
|
|
805
|
-
c.log(logL.INFO, F"set country version: {self.col.country_ver}")
|
|
806
|
-
else:
|
|
807
|
-
c.log(logL.WARN, "was not find <country specific code version> in object_list")
|
|
808
|
-
break
|
|
809
|
-
else:
|
|
810
|
-
c.log(logL.WARN, "was not find <country specific code> in object_list")
|
|
811
|
-
self.col.spec_map = self.col.get_spec()
|
|
812
|
-
for o_l_el in self.obj_list:
|
|
813
|
-
o_l_el: structs.ObjectListElement
|
|
814
|
-
try:
|
|
815
|
-
self.col.add_if_missing( # todo: remove add_if?
|
|
816
|
-
class_id=ut.CosemClassId(int(o_l_el.class_id)),
|
|
817
|
-
version=o_l_el.version,
|
|
818
|
-
logical_name=o_l_el.logical_name)
|
|
819
|
-
except collection.CollectionMapError as e:
|
|
820
|
-
res.append_err(e)
|
|
821
|
-
self.col.add_if_missing(
|
|
822
|
-
class_id=overview.ClassID.DATA,
|
|
823
|
-
version=None, # todo: check version else set 0
|
|
824
|
-
logical_name=cst.LogicalName.from_obis("0.0.42.0.0.255")) # todo: make better
|
|
825
|
-
for ass in self.col.iter_classID_objects(overview.ClassID.ASSOCIATION_LN):
|
|
826
|
-
ass: collection.AssociationLN
|
|
827
|
-
if ass.logical_name.e != 0:
|
|
828
|
-
await ReadObjAttr(ass, 3).exchange(c) # todo: remove from self.queue
|
|
829
|
-
if ass.associated_partners_id.client_SAP == c.SAP:
|
|
830
|
-
cur_ass = ass
|
|
831
|
-
break
|
|
832
|
-
else: # use current association if no variant
|
|
833
|
-
cur_ass = self.col.add_if_missing(
|
|
834
|
-
class_id=overview.ClassID.ASSOCIATION_LN,
|
|
835
|
-
version=None,
|
|
836
|
-
logical_name=cst.LogicalName.from_obis("0.0.40.0.0.255")
|
|
837
|
-
)
|
|
838
|
-
await ReadObjAttr(cur_ass, 3).exchange(c)
|
|
839
|
-
cur_ass.set_attr(2, self.obj_list)
|
|
840
|
-
if cur_ass.associated_partners_id.client_SAP != c.SAP:
|
|
841
|
-
c.log(logL.ERR, F"Wrong current server SAP: {c.SAP} use {cur_ass.associated_partners_id.client_SAP}")
|
|
842
|
-
self.queue.put_nowait((cur_ass, 3)) # read forcibly <associated_partners_id>: use in MapTypeCreator(by <has_sap> method)
|
|
843
|
-
reduce_ln = ln_pattern.LNPattern.parse("0.0.(40,42).0.0.255")
|
|
844
|
-
"""reduced objects for read"""
|
|
845
|
-
for o_l_el in cur_ass.object_list: # todo: read necessary data for create_type
|
|
846
|
-
if reduce_ln == o_l_el.logical_name:
|
|
847
|
-
"""nothing do it"""
|
|
848
|
-
else:
|
|
849
|
-
if (obj := self.col.get(o_l_el.logical_name.contents)) is None:
|
|
850
|
-
continue
|
|
851
|
-
for access in o_l_el.access_rights.attribute_access:
|
|
852
|
-
i = int(access.attribute_id)
|
|
853
|
-
if (
|
|
854
|
-
i == 1 # skip LN
|
|
855
|
-
or not access.access_mode.is_readable() # skip not readable
|
|
856
|
-
or ( # skip early gotten object_list
|
|
857
|
-
cur_ass.logical_name == o_l_el.logical_name
|
|
858
|
-
and i == 2
|
|
859
|
-
) or (
|
|
860
|
-
access.access_mode.is_writable() # skip unknown type writable element
|
|
861
|
-
and not ( # except for:
|
|
862
|
-
isinstance(obj.get_attr_element(i).DATA_TYPE, ut.CHOICE)
|
|
863
|
-
or (
|
|
864
|
-
isinstance(obj, collection.ProfileGeneric)
|
|
865
|
-
and i == 3)
|
|
866
|
-
)
|
|
867
|
-
) or obj.get_attr_element(i).classifier == collection.ic.Classifier.DYNAMIC # skip DYNAMIC
|
|
868
|
-
):
|
|
869
|
-
continue
|
|
870
|
-
self.queue.put_nowait((obj, i))
|
|
871
|
-
for d_id in collection.get_filtered(
|
|
872
|
-
objects=self.col,
|
|
873
|
-
keys=(
|
|
874
|
-
ln_pattern.DEVICE_ID,
|
|
875
|
-
ln_pattern.PROGRAM_ENTRIES)):
|
|
876
|
-
self.queue.put_nowait((d_id, 2)) # todo: make second queue2 for ReadEmptyAttribute(d_id.logical_name, 2).exchange(c)
|
|
877
|
-
except TimeoutError as e:
|
|
878
|
-
c.log(logL.ERR, F"can't got <object list>: {e}")
|
|
879
|
-
finally:
|
|
880
|
-
self.wait_list.release()
|
|
881
|
-
while True:
|
|
882
|
-
try:
|
|
883
|
-
obj, i = self.queue.get_nowait()
|
|
884
|
-
except asyncio.QueueEmpty as e:
|
|
885
|
-
c.log(logL.INFO, "QueueEmpty")
|
|
886
|
-
try:
|
|
887
|
-
await asyncio.wait_for(self.queue.join(), c.com_profile.parameters.inactivity_time_out) # todo: why whis timeout?? # todo: make not custom <inactivity_time_out>
|
|
888
|
-
break
|
|
889
|
-
except TimeoutError as e:
|
|
890
|
-
c.log(logL.INFO, "wait returned tasks in queue")
|
|
891
|
-
continue # wait returned tasks in queue
|
|
892
|
-
try:
|
|
893
|
-
await c.read_attribute(obj, i)
|
|
894
|
-
except TimeoutError as e:
|
|
895
|
-
c.log(logL.ERR, F"return {obj}:{i} in queue: {e}")
|
|
896
|
-
await self.queue.put((obj, i))
|
|
897
|
-
except exc.Timeout as e:
|
|
898
|
-
c.log(logL.ERR, F"break create type {self.col} by {e}")
|
|
899
|
-
await self.queue.put((obj, i))
|
|
900
|
-
raise e
|
|
901
|
-
except AttributeError as e:
|
|
902
|
-
c.log(logL.ERR, F"skip value wrong value for {obj}:{i}: {e}")
|
|
903
|
-
except Exception as e: # todo: make better!!!
|
|
904
|
-
c.log(logL.ERR, F"skip value wrong value for {obj}:{i}: {e}")
|
|
905
|
-
finally:
|
|
906
|
-
self.queue.task_done()
|
|
907
|
-
print("stop create")
|
|
908
|
-
return res
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
type ID_SAP = tuple[collection.ID, enums.ClientSAP]
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
@dataclass(frozen=True)
|
|
915
|
-
class IDSAP:
|
|
916
|
-
id: collection.ID
|
|
917
|
-
sap: enums.ClientSAP
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
@dataclass
|
|
921
|
-
class NonInit:
|
|
922
|
-
msg: str
|
|
923
|
-
|
|
924
|
-
def __getattr__(self, item):
|
|
925
|
-
raise RuntimeError(self.msg)
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
class MapTypeCreator:
|
|
929
|
-
adapter: Adapter
|
|
930
|
-
lock: asyncio.Lock
|
|
931
|
-
con: dict[IDSAP, CreateType]
|
|
932
|
-
|
|
933
|
-
def __init__(self, adapter: Adapter):
|
|
934
|
-
self.adapter = adapter
|
|
935
|
-
self.con = dict()
|
|
936
|
-
self.lock = asyncio.Lock()
|
|
937
|
-
|
|
938
|
-
async def get_collection(
|
|
939
|
-
self,
|
|
940
|
-
c: Client,
|
|
941
|
-
col_id: collection.ID
|
|
942
|
-
) -> result.Simple[collection.Collection]:
|
|
943
|
-
new_col: collection.Collection
|
|
944
|
-
err: Errors
|
|
945
|
-
id_sap = IDSAP(col_id, c.SAP)
|
|
946
|
-
async with self.lock:
|
|
947
|
-
if id_sap in self.con.keys():
|
|
948
|
-
c.log(logL.INFO, F"{self.__class__.__name__} {col_id} already in container")
|
|
949
|
-
else:
|
|
950
|
-
c.log(logL.INFO, F"{self.__class__.__name__} register new collection: {col_id}")
|
|
951
|
-
self.con[id_sap] = CreateType(col_id)
|
|
952
|
-
res = await self.con[id_sap].exchange(c)
|
|
953
|
-
async with self.lock:
|
|
954
|
-
try:
|
|
955
|
-
gotten, _ = self.adapter.get_collection(col_id).unpack() # check for first update
|
|
956
|
-
if gotten.has_sap(id_sap.sap):
|
|
957
|
-
"""not need keep"""
|
|
958
|
-
else:
|
|
959
|
-
self.adapter.set_collection(res.value) # todo: make as ADAPTER.merge_collection(ret)
|
|
960
|
-
except AdapterException as e:
|
|
961
|
-
self.adapter.set_collection(res.value)
|
|
962
|
-
res.value, err = res.value.copy().unpack()
|
|
963
|
-
if err is not None:
|
|
964
|
-
res.append_err(err)
|
|
965
|
-
return res
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
@dataclass
|
|
969
|
-
class InitType(SimpleCopy, Simple[collection.Collection]):
|
|
970
|
-
adapter: Adapter
|
|
971
|
-
msg: str = "initiate type"
|
|
972
|
-
|
|
973
|
-
async def exchange(self, c: Client) -> result.SimpleOrError[collection.Collection]:
|
|
974
|
-
if isinstance((res := await Sequence(
|
|
975
|
-
GetLDN(),
|
|
976
|
-
FindFirmwareId(),
|
|
977
|
-
FindFirmwareVersion(),
|
|
978
|
-
msg="get collection.ID",
|
|
979
|
-
).exchange(c)), result.Error):
|
|
980
|
-
return res.with_msg("init type")
|
|
981
|
-
ldn, f_id, f_ver = res.value
|
|
982
|
-
col_id = collection.ID(ldn.get_manufacturer(), f_id, f_ver)
|
|
983
|
-
try:
|
|
984
|
-
if (res := self.adapter.get_collection(col_id)).value.has_sap(c.SAP):
|
|
985
|
-
c.log(logL.INFO, F"find collection in {self.adapter}")
|
|
986
|
-
else:
|
|
987
|
-
raise AdapterException(F"was found collection from adapter with absent current {c.SAP}")
|
|
988
|
-
except AdapterException as e:
|
|
989
|
-
c.log(logL.WARN, F"not find into adapter: {e}")
|
|
990
|
-
res = await map_type_creator.get_collection(
|
|
991
|
-
c=c,
|
|
992
|
-
col_id=col_id
|
|
993
|
-
)
|
|
994
|
-
c._objects = res.value
|
|
995
|
-
c._objects.LDN.set_attr(2, ldn)
|
|
996
|
-
return res
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
init_type: InitType
|
|
1000
|
-
"""init after get_adapter"""
|
|
1001
|
-
map_type_creator: MapTypeCreator
|
|
1002
|
-
"""init after get_adapter"""
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
def get_adapter(value: Adapter):
|
|
1006
|
-
global map_type_creator, init_type
|
|
1007
|
-
map_type_creator = MapTypeCreator(value)
|
|
1008
|
-
init_type = InitType(value)
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
get_adapter(gag) # Dummy Adapter
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
@dataclass
|
|
1015
|
-
@deprecated("use <ReadObjAttr>")
|
|
1016
|
-
class ReadAttribute(SimpleCopy, CDT):
|
|
1017
|
-
ln: collection.LNContaining
|
|
1018
|
-
index: int
|
|
1019
|
-
msg: str = "read LN attribute"
|
|
1020
|
-
|
|
1021
|
-
async def exchange(self, c: Client) -> result.Simple[cdt.CommonDataType]:
|
|
1022
|
-
obj = c.objects.get_object(self.ln)
|
|
1023
|
-
return await ReadObjAttr(obj, self.index).exchange(c)
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
@dataclass
|
|
1027
|
-
class ReadObjAttr(SimpleCopy, CDT):
|
|
1028
|
-
obj: collection.InterfaceClass
|
|
1029
|
-
index: int
|
|
1030
|
-
msg: str = "read object attribute"
|
|
1031
|
-
|
|
1032
|
-
async def exchange(self, c: Client) -> result.SimpleOrError[cdt.CommonDataType]:
|
|
1033
|
-
# TODO: check is_readable?
|
|
1034
|
-
c.get_get_request_normal(
|
|
1035
|
-
attr_desc=self.obj.get_attr_descriptor(
|
|
1036
|
-
value=self.index,
|
|
1037
|
-
with_selection=bool(c.negotiated_conformance.selective_access)))
|
|
1038
|
-
start_read_time: float = time.perf_counter()
|
|
1039
|
-
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
1040
|
-
return res_pdu
|
|
1041
|
-
c.last_transfer_time = datetime.timedelta(seconds=time.perf_counter()-start_read_time)
|
|
1042
|
-
try:
|
|
1043
|
-
self.obj.set_attr(self.index, res_pdu.value)
|
|
1044
|
-
return result.Simple(self.obj.get_attr(self.index))
|
|
1045
|
-
except ValueError as e:
|
|
1046
|
-
return result.Error.from_e(e)
|
|
1047
|
-
except ut.UserfulTypesException as e:
|
|
1048
|
-
return result.Error.from_e(e)
|
|
1049
|
-
except exc.DLMSException as e:
|
|
1050
|
-
return result.Error.from_e(e)
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
@dataclass(frozen=True)
|
|
1054
|
-
class Par2Data[T: cdt.CommonDataType](SimpleCopy, CDT[T]):
|
|
1055
|
-
"""get CommonDataType by Parameter"""
|
|
1056
|
-
par: Parameter
|
|
1057
|
-
msg: str = "read data by Parameter"
|
|
1058
|
-
|
|
1059
|
-
async def exchange(self, c: Client) -> result.SimpleOrError[T]:
|
|
1060
|
-
if isinstance((res_obj := c.objects.par2obj(self.par)), result.Error):
|
|
1061
|
-
return res_obj
|
|
1062
|
-
if isinstance((res := await ReadObjAttr(res_obj.value, self.par.i).exchange(c)), result.Error):
|
|
1063
|
-
return res
|
|
1064
|
-
for el in self.par.elements():
|
|
1065
|
-
res.value = res.value[el]
|
|
1066
|
-
return res
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
class ReadSequence(List):
|
|
1070
|
-
tasks: list[ReadAttribute]
|
|
1071
|
-
|
|
1072
|
-
def __post_init__(self):
|
|
1073
|
-
assert all((isinstance(t, ReadAttribute) for t in self.tasks))
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
type AttrValueComp = Callable[[cdt.CommonDataType | None], bool]
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
def is_empty(value: cdt.CommonDataType | None) -> bool:
|
|
1080
|
-
"""is empty attribute value"""
|
|
1081
|
-
return True if value is None else False
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
@dataclass(frozen=True)
|
|
1085
|
-
class ReadAttributeIf(SimpleCopy, Base):
|
|
1086
|
-
"""read if func with arg as value is True"""
|
|
1087
|
-
ln: collection.LNContaining
|
|
1088
|
-
index: int
|
|
1089
|
-
func: AttrValueComp
|
|
1090
|
-
msg: str = "read attribute with condition"
|
|
1091
|
-
|
|
1092
|
-
async def exchange(self, c: Client) -> result.Simple[cdt.CommonDataType]:
|
|
1093
|
-
# TODO: check is_readable
|
|
1094
|
-
obj = c.objects.get_object(self.ln)
|
|
1095
|
-
if self.func(obj.get_attr(self.index)):
|
|
1096
|
-
return await ReadAttribute(
|
|
1097
|
-
ln=self.ln,
|
|
1098
|
-
index=self.index).exchange(c)
|
|
1099
|
-
else:
|
|
1100
|
-
return result.OK
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
@dataclass(frozen=True)
|
|
1104
|
-
class ReadEmptyAttribute(SimpleCopy, Base):
|
|
1105
|
-
ln: collection.LNContaining
|
|
1106
|
-
index: int
|
|
1107
|
-
msg: str = "read if attribute is empty"
|
|
1108
|
-
|
|
1109
|
-
async def exchange(self, c: Client) -> result.Simple[cdt.CommonDataType]:
|
|
1110
|
-
# TODO: check is_readable
|
|
1111
|
-
return await ReadAttributeIf(
|
|
1112
|
-
ln=self.ln,
|
|
1113
|
-
index=self.index,
|
|
1114
|
-
func=is_empty
|
|
1115
|
-
).exchange(c)
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
@dataclass(frozen=True)
|
|
1119
|
-
class ReadWritableAttributes(SimpleCopy, Base):
|
|
1120
|
-
ln: collection.LNContaining
|
|
1121
|
-
indexes: tuple[int, ...]
|
|
1122
|
-
msg: str = "read only writable attribute"
|
|
1123
|
-
|
|
1124
|
-
async def exchange(self, c: Client) -> result.List[cdt.CommonDataType]:
|
|
1125
|
-
# TODO: check is_readable
|
|
1126
|
-
res = result.List()
|
|
1127
|
-
indexes: list[int] = []
|
|
1128
|
-
ass: collection.AssociationLN = c._objects.sap2association(c.SAP)
|
|
1129
|
-
for i in self.indexes:
|
|
1130
|
-
if ass.is_writable(self.ln, i):
|
|
1131
|
-
indexes.append(i)
|
|
1132
|
-
if len(indexes) != 0:
|
|
1133
|
-
res.append(await ReadAttributes(
|
|
1134
|
-
ln=self.ln,
|
|
1135
|
-
indexes=tuple(indexes)
|
|
1136
|
-
).exchange(c))
|
|
1137
|
-
else:
|
|
1138
|
-
res.append(result.OK)
|
|
1139
|
-
c.log(logL.INFO, F"skip {self.__class__.__name__} operation, all is actually")
|
|
1140
|
-
return res
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
# copy past from ReadWritableAttributes
|
|
1144
|
-
@dataclass(frozen=True)
|
|
1145
|
-
class ActualizeAttributes(SimpleCopy, OK):
|
|
1146
|
-
ln: collection.LNContaining
|
|
1147
|
-
indexes: tuple[int, ...]
|
|
1148
|
-
msg: str = "read if attribute is empty or writable"
|
|
1149
|
-
|
|
1150
|
-
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1151
|
-
# TODO: check is_readable
|
|
1152
|
-
ass: collection.AssociationLN = c.objects.sap2association(c.SAP)
|
|
1153
|
-
obj = c.objects.get_object(self.ln)
|
|
1154
|
-
indexes = [
|
|
1155
|
-
i for i in self.indexes if (
|
|
1156
|
-
obj.get_attr(i) is None
|
|
1157
|
-
or obj.get_attr_element(i).classifier == ic.Classifier.DYNAMIC
|
|
1158
|
-
or ass.is_writable(self.ln, i)
|
|
1159
|
-
)
|
|
1160
|
-
]
|
|
1161
|
-
if len(indexes) != 0:
|
|
1162
|
-
if isinstance((res := await ReadAttributes(
|
|
1163
|
-
ln=self.ln,
|
|
1164
|
-
indexes=tuple(indexes)
|
|
1165
|
-
).exchange(c)), result.Error):
|
|
1166
|
-
return res
|
|
1167
|
-
return result.OK
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
@dataclass(frozen=True)
|
|
1171
|
-
class ReadAttributes(SimpleCopy, _List[cdt.CommonDataType]):
|
|
1172
|
-
ln: collection.LNContaining
|
|
1173
|
-
indexes: tuple[int, ...]
|
|
1174
|
-
msg: str = ""
|
|
1175
|
-
|
|
1176
|
-
async def exchange(self, c: Client) -> result.List[cdt.CommonDataType] | result.Error:
|
|
1177
|
-
res = result.List()
|
|
1178
|
-
obj = c.objects.get_object(self.ln)
|
|
1179
|
-
# TODO: check for Get-Request-With-List
|
|
1180
|
-
for i in self.indexes:
|
|
1181
|
-
res.append(await ReadObjAttr(obj, i).exchange(c))
|
|
1182
|
-
return res
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
@dataclass
|
|
1186
|
-
@deprecated("use <Write2>")
|
|
1187
|
-
class Write(SimpleCopy, OK):
|
|
1188
|
-
"""write with ParameterData struct"""
|
|
1189
|
-
par_data: ParData
|
|
1190
|
-
msg: str = "write attribute"
|
|
1191
|
-
|
|
1192
|
-
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1193
|
-
if isinstance(res_obj := c.objects.par2obj(self.par_data.par), result.Error):
|
|
1194
|
-
return res_obj
|
|
1195
|
-
if self.par_data.par.n_elements == 0:
|
|
1196
|
-
enc = self.par_data.data.encoding
|
|
1197
|
-
elif isinstance(res_read := await ReadObjAttr(res_obj.value, self.par_data.par.i).exchange(c), result.Error):
|
|
1198
|
-
return res_read
|
|
1199
|
-
else:
|
|
1200
|
-
data = a_data
|
|
1201
|
-
for el in self.par_data.par.elements():
|
|
1202
|
-
data = data[el]
|
|
1203
|
-
data.set(self.par_data.data)
|
|
1204
|
-
enc = data.encoding
|
|
1205
|
-
data = c.get_set_request_normal(
|
|
1206
|
-
obj=res_obj.value,
|
|
1207
|
-
attr_index=self.par_data.par.i,
|
|
1208
|
-
value=enc)
|
|
1209
|
-
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
1210
|
-
return res_pdu
|
|
1211
|
-
return result.OK
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
@dataclass(frozen=True)
|
|
1215
|
-
class Write2(SimpleCopy, OK):
|
|
1216
|
-
"""write with ParameterData struct"""
|
|
1217
|
-
par: Parameter
|
|
1218
|
-
data: cdt.CommonDataType
|
|
1219
|
-
msg: str = "write Data"
|
|
1220
|
-
|
|
1221
|
-
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1222
|
-
if isinstance(res_obj := c.objects.par2obj(self.par), result.Error):
|
|
1223
|
-
return res_obj
|
|
1224
|
-
if self.par.n_elements == 0:
|
|
1225
|
-
enc = self.data.encoding
|
|
1226
|
-
elif isinstance(res_read := await Par2Data[cdt.CommonDataType](self.par.attr).exchange(c), result.Error):
|
|
1227
|
-
return res_read
|
|
1228
|
-
else:
|
|
1229
|
-
data = res_read.value
|
|
1230
|
-
for el in self.par.elements():
|
|
1231
|
-
data = data[el]
|
|
1232
|
-
data.set(self.data)
|
|
1233
|
-
enc = data.encoding
|
|
1234
|
-
data = c.get_set_request_normal(
|
|
1235
|
-
obj=res_obj.value,
|
|
1236
|
-
attr_index=self.par.i,
|
|
1237
|
-
value=enc)
|
|
1238
|
-
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
1239
|
-
return res_pdu
|
|
1240
|
-
return result.OK
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
@dataclass(frozen=True)
|
|
1244
|
-
class SetBits(SimpleCopy, OK):
|
|
1245
|
-
"""set bits by pattern"""
|
|
1246
|
-
par: Parameter
|
|
1247
|
-
pattern: dict[int, int]
|
|
1248
|
-
msg: str = "set bits"
|
|
1249
|
-
|
|
1250
|
-
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1251
|
-
if isinstance(res_obj := c.objects.par2obj(self.par), result.Error):
|
|
1252
|
-
return res_obj
|
|
1253
|
-
if isinstance(res_read := await Par2Data[cdt.Digital | cdt.BitString](self.par.attr).exchange(c), result.Error):
|
|
1254
|
-
return res_read
|
|
1255
|
-
data = res_read.value
|
|
1256
|
-
for el in self.par.elements():
|
|
1257
|
-
data = data[el]
|
|
1258
|
-
if isinstance(data, cdt.Digital):
|
|
1259
|
-
data_int = int(data)
|
|
1260
|
-
for pos, val in self.pattern.items():
|
|
1261
|
-
data_int = (data_int | (1 << pos)) if val else (data_int & ~(1 << pos))
|
|
1262
|
-
data.set(data_int)
|
|
1263
|
-
elif isinstance(data, cdt.BitString):
|
|
1264
|
-
for pos, val in self.pattern.items():
|
|
1265
|
-
try:
|
|
1266
|
-
data[pos] = val
|
|
1267
|
-
except IndexError as e:
|
|
1268
|
-
return result.Error.from_e(ValueError(f"can't set bit {pos} in {self.par}"))
|
|
1269
|
-
else:
|
|
1270
|
-
return result.Error.from_e(TypeError(f"{self.par} not available SetBits"))
|
|
1271
|
-
c.get_set_request_normal(
|
|
1272
|
-
obj=res_obj.value,
|
|
1273
|
-
attr_index=self.par.i,
|
|
1274
|
-
value=res_read.value.encoding)
|
|
1275
|
-
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
1276
|
-
return res_pdu
|
|
1277
|
-
return result.OK
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
@dataclass
|
|
1281
|
-
@deprecated("use <WriteTranscript>")
|
|
1282
|
-
class WriteParValue(SimpleCopy, OK):
|
|
1283
|
-
"""write with ParameterValues struct"""
|
|
1284
|
-
par_value: ParValues[cdt.Transcript]
|
|
1285
|
-
msg: str = "write attribute by Transcript"
|
|
1286
|
-
|
|
1287
|
-
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1288
|
-
if isinstance(res1 := c.objects.par2obj(self.par_value.par), result.Error):
|
|
1289
|
-
return res1
|
|
1290
|
-
obj = res1.value
|
|
1291
|
-
if (a_data := res1.value.get_attr(self.par_value.par.i)) is None:
|
|
1292
|
-
if isinstance((res := await Par2Data(self.par_value.par).exchange(c)), result.Error):
|
|
1293
|
-
return res
|
|
1294
|
-
else:
|
|
1295
|
-
a_data = res.value
|
|
1296
|
-
s_u = c._objects.par2su(self.par_value.par)
|
|
1297
|
-
if isinstance(s_u, cdt.ScalUnitType):
|
|
1298
|
-
value: cdt.Transcript = str(float(self.par_value.data) * 10 ** -int(s_u.scaler))
|
|
1299
|
-
else:
|
|
1300
|
-
value = self.par_value.data
|
|
1301
|
-
if self.par_value.par.n_elements == 0:
|
|
1302
|
-
set_data = a_data.parse(value)
|
|
1303
|
-
elif isinstance((res := await Par2Data(par).exchange(c)), result.Error):
|
|
1304
|
-
return res
|
|
1305
|
-
else:
|
|
1306
|
-
data = a_data
|
|
1307
|
-
for el in self.par_value.par.elements():
|
|
1308
|
-
data = data[el]
|
|
1309
|
-
new_data = data.parse(value) # todo: can't use with CHOICE
|
|
1310
|
-
data.set(new_data)
|
|
1311
|
-
set_data = a_data
|
|
1312
|
-
data = c.get_set_request_normal(
|
|
1313
|
-
obj=obj,
|
|
1314
|
-
attr_index=self.par_value.par.i,
|
|
1315
|
-
value=set_data.encoding)
|
|
1316
|
-
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
1317
|
-
return res_pdu
|
|
1318
|
-
return result.OK
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
@dataclass
|
|
1322
|
-
@deprecated("use <Write>")
|
|
1323
|
-
class WriteAttribute(SimpleCopy, OK):
|
|
1324
|
-
ln: collection.LNContaining
|
|
1325
|
-
index: int
|
|
1326
|
-
value: bytes | str | int | list | tuple | datetime.datetime
|
|
1327
|
-
msg: str = ""
|
|
1328
|
-
|
|
1329
|
-
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1330
|
-
obj = c._objects.get_object(self.ln)
|
|
1331
|
-
if isinstance(self.value, (str, int, list, tuple, datetime.datetime)):
|
|
1332
|
-
value2 = await c.encode(
|
|
1333
|
-
obj=obj,
|
|
1334
|
-
index=self.index,
|
|
1335
|
-
value=self.value)
|
|
1336
|
-
enc = value2.encoding
|
|
1337
|
-
else:
|
|
1338
|
-
enc = self.value
|
|
1339
|
-
data = c.get_set_request_normal(
|
|
1340
|
-
obj=obj,
|
|
1341
|
-
attr_index=self.index,
|
|
1342
|
-
value=enc)
|
|
1343
|
-
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
1344
|
-
return res_pdu
|
|
1345
|
-
return result.OK # todo: return Data-Access-Result
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
@deprecated("use Execute")
|
|
1349
|
-
@dataclass
|
|
1350
|
-
class ExecuteByDesc(SimpleCopy, Base):
|
|
1351
|
-
"""execute method by method descriptor # TODO: rewrite this"""
|
|
1352
|
-
desc: ut.CosemMethodDescriptor
|
|
1353
|
-
msg: str = "old execute"
|
|
1354
|
-
|
|
1355
|
-
async def exchange(self, c: Client) -> result.Result:
|
|
1356
|
-
try:
|
|
1357
|
-
await c.execute_method(self.desc)
|
|
1358
|
-
return result.Simple(pdu.ActionResult.SUCCESS)
|
|
1359
|
-
except Exception as e:
|
|
1360
|
-
return result.Error.from_e(exc.DLMSException(F'Исполнение {self.desc}'))
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
@deprecated("use Execute2")
|
|
1364
|
-
@dataclass
|
|
1365
|
-
class Execute(SimpleCopy, OK):
|
|
1366
|
-
"""execute method"""
|
|
1367
|
-
ln: collection.LNContaining
|
|
1368
|
-
index: int
|
|
1369
|
-
value: cdt.CommonDataType = None # todo: maybe use simple bytes
|
|
1370
|
-
msg: str = "execute method"
|
|
1371
|
-
|
|
1372
|
-
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1373
|
-
obj = c._objects.get_object(self.ln)
|
|
1374
|
-
try:
|
|
1375
|
-
await c.execute_method2(obj, self.index, self.value)
|
|
1376
|
-
return result.OK
|
|
1377
|
-
except Exception as e:
|
|
1378
|
-
return result.Error.from_e(exc.DLMSException(F'Исполнение {self.desc}'))
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
@dataclass(frozen=True)
|
|
1382
|
-
class Execute2(SimpleCopy, OK):
|
|
1383
|
-
"""execute method"""
|
|
1384
|
-
par: Parameter
|
|
1385
|
-
data: cdt.CommonDataType
|
|
1386
|
-
msg: str = "Execute method"
|
|
1387
|
-
|
|
1388
|
-
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1389
|
-
if isinstance(res := c.objects.par2obj(self.par), result.Error):
|
|
1390
|
-
return res
|
|
1391
|
-
try:
|
|
1392
|
-
data = c.get_action_request_normal(
|
|
1393
|
-
meth_desc=res.value.get_meth_descriptor(self.par.i),
|
|
1394
|
-
method=self.data
|
|
1395
|
-
)
|
|
1396
|
-
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
1397
|
-
return res_pdu
|
|
1398
|
-
return result.OK
|
|
1399
|
-
except Exception as e:
|
|
1400
|
-
return result.Error.from_e(e)
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
@dataclass
|
|
1404
|
-
class GetTimeDelta(SimpleCopy, Simple[float]):
|
|
1405
|
-
"""Read and return <time delta> in second: """
|
|
1406
|
-
msg: str = "Read Clock.time"
|
|
1407
|
-
|
|
1408
|
-
async def exchange(self, c: Client) -> result.SimpleOrError[float]:
|
|
1409
|
-
acc = result.ErrorAccumulator()
|
|
1410
|
-
obj = c._objects.clock
|
|
1411
|
-
if isinstance(
|
|
1412
|
-
res_read_tz := await ReadObjAttr(
|
|
1413
|
-
obj=obj,
|
|
1414
|
-
index=3
|
|
1415
|
-
).exchange(c),
|
|
1416
|
-
result.Error):
|
|
1417
|
-
return res_read_tz
|
|
1418
|
-
tz = datetime.timezone(datetime.timedelta(minutes=int(res_read_tz.value)))
|
|
1419
|
-
if isinstance(
|
|
1420
|
-
res_read := await ReadObjAttr(
|
|
1421
|
-
obj=obj,
|
|
1422
|
-
index=2
|
|
1423
|
-
).exchange(c),
|
|
1424
|
-
result.Error):
|
|
1425
|
-
return res_read
|
|
1426
|
-
value = datetime.datetime.now(tz=tz)
|
|
1427
|
-
value2 = res_read.value.to_datetime().replace(tzinfo=tz)
|
|
1428
|
-
return result.Simple((value2 - value).total_seconds()).append_err(acc.err)
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
@dataclass
|
|
1433
|
-
class WriteTime(SimpleCopy, Simple[float]):
|
|
1434
|
-
"""Write and return <record time> in second: """
|
|
1435
|
-
limit: float = 5.0
|
|
1436
|
-
number_of_retries: int = 10
|
|
1437
|
-
msg: str = "write Clock.time"
|
|
1438
|
-
|
|
1439
|
-
async def exchange(self, c: Client) -> result.SimpleOrError[float]:
|
|
1440
|
-
acc = result.ErrorAccumulator()
|
|
1441
|
-
obj = c._objects.clock
|
|
1442
|
-
c.get_get_request_normal(obj.get_attr_descriptor(3))
|
|
1443
|
-
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
1444
|
-
return res_pdu
|
|
1445
|
-
tz = obj.get_attr_element(3).DATA_TYPE(res_pdu.value)
|
|
1446
|
-
for i in range(self.number_of_retries):
|
|
1447
|
-
pre_time = time.time()
|
|
1448
|
-
if isinstance(
|
|
1449
|
-
res_write := await WriteAttribute(
|
|
1450
|
-
ln=obj.logical_name,
|
|
1451
|
-
index=2,
|
|
1452
|
-
value=(datetime.datetime.utcnow() + datetime.timedelta(minutes=int(tz)))).exchange(c),
|
|
1453
|
-
result.Error):
|
|
1454
|
-
return res_write
|
|
1455
|
-
rec_time = time.time() - pre_time
|
|
1456
|
-
if rec_time < self.limit:
|
|
1457
|
-
break
|
|
1458
|
-
acc.append_e(TimeoutError(f"can't write in {i} attemp in time"))
|
|
1459
|
-
else:
|
|
1460
|
-
return result.Error.from_e(TimeoutError(f"can't write time for limit: {self.limit} second"))
|
|
1461
|
-
return result.Simple(rec_time).append_err(acc.err)
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
@dataclass
|
|
1465
|
-
class ImageTransfer(SimpleCopy, StrictOK):
|
|
1466
|
-
par: dlms_par.ImageTransfer
|
|
1467
|
-
image: bytes
|
|
1468
|
-
waiting_for_activation: float = 10.0
|
|
1469
|
-
n_t_b: int = field(init=False, default=0)
|
|
1470
|
-
"""not transferred block"""
|
|
1471
|
-
n_blocks: int = field(init=False)
|
|
1472
|
-
msg: str = "image transfer"
|
|
1473
|
-
|
|
1474
|
-
def __post_init__(self) -> None:
|
|
1475
|
-
self.ITI = ImageTransferInitiate((
|
|
1476
|
-
bytearray(hashlib.md5(self.image).digest()), # todo: make custom this
|
|
1477
|
-
cdt.DoubleLongUnsigned(len(self.image))
|
|
1478
|
-
))
|
|
1479
|
-
|
|
1480
|
-
async def exchange(self, c: Client) -> result.StrictOk | result.Error:
|
|
1481
|
-
""" update image if blocks is fulls ver 3"""
|
|
1482
|
-
offset: int
|
|
1483
|
-
res_block_size: result.SimpleOrError[cdt.DoubleLongUnsigned]
|
|
1484
|
-
res_status: result.SimpleOrError[i_t_status.ImageTransferStatus]
|
|
1485
|
-
res_activate_info: result.SimpleOrError[ImageToActivateInfo]
|
|
1486
|
-
res_ntb: result.SimpleOrError[cdt.DoubleLongUnsigned]
|
|
1487
|
-
res_tbs: result.SimpleOrError[cdt.BitString]
|
|
1488
|
-
previous_status: Optional[i_t_status.ImageTransferStatus] = None
|
|
1489
|
-
res = result.StrictOk()
|
|
1490
|
-
if isinstance(res_block_size := await Par2Data(self.par.image_block_size).exchange(c), result.Error):
|
|
1491
|
-
return res_block_size
|
|
1492
|
-
block_size = int(res_block_size.value)
|
|
1493
|
-
self.n_blocks, mod = divmod(len(self.image), block_size)
|
|
1494
|
-
if mod != 0:
|
|
1495
|
-
self.n_blocks += 1
|
|
1496
|
-
# TODO: need common counter for exit from infinity loop
|
|
1497
|
-
if isinstance(res_status := await Par2Data(self.par.image_transfer_status).exchange(c), result.Error):
|
|
1498
|
-
return res_status
|
|
1499
|
-
if isinstance(res_activate_info := await Par2Data(self.par.image_to_activate_info).exchange(c), result.Error):
|
|
1500
|
-
return res_activate_info
|
|
1501
|
-
if (
|
|
1502
|
-
res_status.value in (i_t_status.TRANSFER_NOT_INITIATED, i_t_status.VERIFICATION_FAILED, i_t_status.ACTIVATION_FAILED)
|
|
1503
|
-
or len(res_activate_info.value) == 0
|
|
1504
|
-
or res_activate_info.value[0].image_to_activate_identification != self.ITI.image_identifier
|
|
1505
|
-
):
|
|
1506
|
-
if isinstance(res_initiate := await Execute2(self.par.image_transfer_initiate, self.ITI).exchange(c), result.Error):
|
|
1507
|
-
return res_initiate
|
|
1508
|
-
c.log(logL.INFO, "Start initiate Image Transfer")
|
|
1509
|
-
if isinstance(res_status := await Par2Data(self.par.image_transfer_status).exchange(c), result.Error):
|
|
1510
|
-
return res_status
|
|
1511
|
-
elif res_status.value == i_t_status.ACTIVATION_SUCCESSFUL:
|
|
1512
|
-
# image in outside memory and already activated early, but erased by hard programming. Need again go to activation
|
|
1513
|
-
res_status.value = i_t_status.VERIFICATION_SUCCESSFUL
|
|
1514
|
-
else:
|
|
1515
|
-
c.log(logL.INFO, "already INITIATED")
|
|
1516
|
-
if isinstance(res_ntb := await Par2Data(self.par.image_first_not_transferred_block_number).exchange(c), result.Error):
|
|
1517
|
-
return res_ntb
|
|
1518
|
-
self.n_t_b = int(res_ntb.value)
|
|
1519
|
-
if self.n_t_b > (len(self.image) / block_size): # all blocks were send
|
|
1520
|
-
if isinstance(res_verify := await VerifyImage(self.par).exchange(c), result.Error):
|
|
1521
|
-
return res_verify
|
|
1522
|
-
c.log(logL.INFO, "Start Verify Transfer")
|
|
1523
|
-
while True:
|
|
1524
|
-
c.log(logL.STATE, F"{res_status.value=}")
|
|
1525
|
-
match res_status.value:
|
|
1526
|
-
case i_t_status.VERIFICATION_FAILED if res_status.value == previous_status:
|
|
1527
|
-
return result.Error.from_e(exc.DLMSException(), "Verification Error")
|
|
1528
|
-
case i_t_status.TRANSFER_INITIATED if res_status.value == previous_status:
|
|
1529
|
-
res.append_e(exc.DLMSException("Expected Switch to Verification Initiated status, got Initiated"))
|
|
1530
|
-
case i_t_status.TRANSFER_NOT_INITIATED:
|
|
1531
|
-
res.append_e(exc.DLMSException("Got Not initiated status after call Initiation"))
|
|
1532
|
-
case i_t_status.TRANSFER_INITIATED:
|
|
1533
|
-
while self.n_t_b < self.n_blocks:
|
|
1534
|
-
offset = self.n_t_b * block_size
|
|
1535
|
-
if isinstance(res_tr_block := await TransferBlock(
|
|
1536
|
-
par=self.par,
|
|
1537
|
-
number=cdt.DoubleLongUnsigned(self.n_t_b),
|
|
1538
|
-
value=cdt.OctetString(bytearray(self.image[offset: offset + block_size]))
|
|
1539
|
-
).exchange(c), result.Error):
|
|
1540
|
-
return res_tr_block
|
|
1541
|
-
self.n_t_b += 1 # todo: maybe get from SERVER - await get_not_transferred_block.exchange(c)
|
|
1542
|
-
c.log(logL.INFO, "All blocks transferred")
|
|
1543
|
-
if isinstance(res_verify := await VerifyImage(self.par).exchange(c), result.Error):
|
|
1544
|
-
return res_verify
|
|
1545
|
-
case i_t_status.VERIFICATION_INITIATED:
|
|
1546
|
-
c.log(logL.INFO, "read bitstring. It must grow")
|
|
1547
|
-
# TODO: calculate time for waiting or read growing bitstring ?
|
|
1548
|
-
if isinstance(res_tbs := await Par2Data(self.par.image_transferred_blocks_status).exchange(c), result.Error):
|
|
1549
|
-
return res_tbs
|
|
1550
|
-
if len(res_tbs.value) < self.n_t_b:
|
|
1551
|
-
c.log(logL.INFO, F"Got blocks[{len(res_tbs.value)}]") # todo: remove
|
|
1552
|
-
else:
|
|
1553
|
-
c.log(logL.INFO, "All Bits solved")
|
|
1554
|
-
case i_t_status.VERIFICATION_FAILED:
|
|
1555
|
-
if isinstance(res_tbs := await Par2Data(self.par.image_transferred_blocks_status).exchange(c), result.Error):
|
|
1556
|
-
return res_tbs
|
|
1557
|
-
valid = tuple(res_tbs.value)
|
|
1558
|
-
c.log(logL.INFO, F"Got blocks[{len(res_tbs.value)}]") # todo: remove
|
|
1559
|
-
for i in filter(lambda it: valid[it] == b'\x00', range(len(valid))):
|
|
1560
|
-
offset = i * block_size
|
|
1561
|
-
if isinstance(res_tr_block := await TransferBlock(
|
|
1562
|
-
par=self.par,
|
|
1563
|
-
number=cdt.DoubleLongUnsigned(i),
|
|
1564
|
-
value=cdt.OctetString(bytearray(self.image[offset: offset + block_size]))
|
|
1565
|
-
).exchange(c), result.Error):
|
|
1566
|
-
return res_tr_block
|
|
1567
|
-
if isinstance(res_verify := await VerifyImage(self.par).exchange(c), result.Error):
|
|
1568
|
-
return res_verify
|
|
1569
|
-
c.log(logL.INFO, "Start Verify Transfer")
|
|
1570
|
-
case i_t_status.VERIFICATION_SUCCESSFUL:
|
|
1571
|
-
if isinstance(res_tbs := await Par2Data(self.par.image_transferred_blocks_status).exchange(c), result.Error):
|
|
1572
|
-
return res_tbs
|
|
1573
|
-
valid = tuple(res_tbs.value)
|
|
1574
|
-
if isinstance(res_ntb := await Par2Data(self.par.image_first_not_transferred_block_number).exchange(c), result.Error):
|
|
1575
|
-
return res_ntb
|
|
1576
|
-
self.n_t_b = int(res_ntb.value)
|
|
1577
|
-
if isinstance(res_activate_info := await Par2Data(self.par.image_to_activate_info).exchange(c), result.Error):
|
|
1578
|
-
return res_activate_info
|
|
1579
|
-
c.log(logL.INFO, F"md5:{res_activate_info.value[0].image_to_activate_signature}")
|
|
1580
|
-
if any(map(lambda it: it == b'\x00', valid)):
|
|
1581
|
-
if isinstance(res_initiate := await Execute2(self.par.image_transfer_initiate, self.ITI).exchange(c), result.Error):
|
|
1582
|
-
return res_initiate
|
|
1583
|
-
c.log(logL.INFO, "Start initiate Image Transfer, after wrong verify. Exist 0 blocks")
|
|
1584
|
-
return res.as_error(exc.DLMSException("Exist 0 blocks"))
|
|
1585
|
-
elif self.n_t_b < len(valid):
|
|
1586
|
-
if isinstance(res_initiate := await Execute2(self.par.image_transfer_initiate, self.ITI).exchange(c), result.Error):
|
|
1587
|
-
return res_initiate
|
|
1588
|
-
c.log(logL.INFO, "Start initiate Image Transfer, after wrong verify. Got not transferred block")
|
|
1589
|
-
return res.as_error(exc.DLMSException(F"Got {res_ntb.value} not transferred block"))
|
|
1590
|
-
elif res_activate_info.value[0].image_to_activate_signature != res_activate_info.value[0].image_to_activate_identification:
|
|
1591
|
-
if isinstance(res_initiate := await Execute2(self.par.image_transfer_initiate, self.ITI).exchange(c), result.Error):
|
|
1592
|
-
return res_initiate
|
|
1593
|
-
return res.as_error(exc.DLMSException(
|
|
1594
|
-
F"Signature not match to Identification: got {res_activate_info.value[0].image_to_activate_signature}, "
|
|
1595
|
-
F"expected {res_activate_info.value[0].image_to_activate_identification}"),
|
|
1596
|
-
"Start initiate Image Transfer, after wrong verify")
|
|
1597
|
-
else:
|
|
1598
|
-
if isinstance(res_activate := await ActivateImage(self.par).exchange(c), result.Error):
|
|
1599
|
-
return res_activate
|
|
1600
|
-
c.log(logL.INFO, "Start Activate Transfer")
|
|
1601
|
-
case i_t_status.ACTIVATION_INITIATED:
|
|
1602
|
-
try:
|
|
1603
|
-
await c.disconnect_request()
|
|
1604
|
-
except TimeoutError as e:
|
|
1605
|
-
c.log(logL.ERR, F"can't use <disconnect request>: {e}")
|
|
1606
|
-
if isinstance(res_reconnect := await HardwareReconnect(
|
|
1607
|
-
delay=self.waiting_for_activation,
|
|
1608
|
-
msg="expected reboot server after upgrade"
|
|
1609
|
-
).exchange(c), result.Error):
|
|
1610
|
-
return res_reconnect
|
|
1611
|
-
case i_t_status.ACTIVATION_SUCCESSFUL:
|
|
1612
|
-
if isinstance(res_activate_info := await Par2Data(self.par.image_to_activate_info).exchange(c), result.Error):
|
|
1613
|
-
return res_activate_info
|
|
1614
|
-
if res_activate_info.value[0].image_to_activate_identification == self.ITI.image_identifier:
|
|
1615
|
-
c.log(logL.INFO, "already activated this image")
|
|
1616
|
-
return res
|
|
1617
|
-
else:
|
|
1618
|
-
if isinstance(res_initiate := await Execute2(self.par.image_transfer_initiate, self.ITI).exchange(c), result.Error):
|
|
1619
|
-
return res_initiate
|
|
1620
|
-
c.log(logL.INFO, "Start initiate Image Transfer")
|
|
1621
|
-
# TODO: need wait clearing memory in device ~5 sec
|
|
1622
|
-
case i_t_status.ACTIVATION_FAILED:
|
|
1623
|
-
return res.as_error(exc.DLMSException(), "Ошибка активации...")
|
|
1624
|
-
case err:
|
|
1625
|
-
return res.as_error(exc.DLMSException(), f"Unknown image transfer status: {err}")
|
|
1626
|
-
previous_status = res_status.value
|
|
1627
|
-
await asyncio.sleep(1) # TODO: tune it
|
|
1628
|
-
if isinstance(res_status := await Par2Data(self.par.image_transfer_status).exchange(c), result.Error):
|
|
1629
|
-
return res_status
|
|
1630
|
-
c.log(logL.INFO, f"{res_status.value=}")
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
@dataclass(frozen=True)
|
|
1634
|
-
class TransferBlock(SimpleCopy, OK):
|
|
1635
|
-
par: dlms_par.ImageTransfer
|
|
1636
|
-
number: cdt.DoubleLongUnsigned
|
|
1637
|
-
value: cdt.OctetString
|
|
1638
|
-
msg: str = "transfer block"
|
|
1639
|
-
|
|
1640
|
-
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1641
|
-
if isinstance(res := await Execute2(
|
|
1642
|
-
par=self.par.image_block_transfer,
|
|
1643
|
-
data=ImageBlockTransfer((self.number, self.value))
|
|
1644
|
-
).exchange(c), result.Error):
|
|
1645
|
-
return res
|
|
1646
|
-
return result.OK
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
@dataclass(frozen=True)
|
|
1650
|
-
class VerifyImage(SimpleCopy, OK):
|
|
1651
|
-
par: dlms_par.ImageTransfer
|
|
1652
|
-
msg: str = "Verify image"
|
|
1653
|
-
|
|
1654
|
-
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1655
|
-
if isinstance(res := await Execute2(self.par.image_verify, integers.INTEGER_0).exchange(c), result.Error):
|
|
1656
|
-
return res
|
|
1657
|
-
return result.OK
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
@dataclass(frozen=True)
|
|
1661
|
-
class ActivateImage(SimpleCopy, OK):
|
|
1662
|
-
par: dlms_par.ImageTransfer
|
|
1663
|
-
msg: str = "Activate image"
|
|
1664
|
-
|
|
1665
|
-
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1666
|
-
if isinstance(res := await Execute2(self.par.image_activate, integers.INTEGER_0).exchange(c), result.Error):
|
|
1667
|
-
return res
|
|
1668
|
-
return result.OK
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
# todo: don't work with new API, remake
|
|
1672
|
-
class TestAll(OK):
|
|
1673
|
-
"""read all attributes with check access""" # todo: add Write with access
|
|
1674
|
-
msg: str = "test all"
|
|
1675
|
-
|
|
1676
|
-
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1677
|
-
# todo: refactoring with <append_err>
|
|
1678
|
-
res = result.ErrorAccumulator()
|
|
1679
|
-
if isinstance(res_objects := c.objects.sap2objects(c.SAP), result.Error):
|
|
1680
|
-
return res_objects
|
|
1681
|
-
ass: collection.AssociationLN = c.objects.sap2association(c.SAP)
|
|
1682
|
-
for obj in res_objects.value:
|
|
1683
|
-
indexes: list[int] = [i for i, _ in obj.get_index_with_attributes()]
|
|
1684
|
-
c.log(logL.INFO, F"start read {obj} attr: {', '.join(map(str, indexes))}")
|
|
1685
|
-
for i in indexes:
|
|
1686
|
-
is_readable = ass.is_readable(
|
|
1687
|
-
ln=obj.logical_name,
|
|
1688
|
-
index=i)
|
|
1689
|
-
if isinstance(res_read := await ReadObjAttr(obj, i).exchange(c), result.Error):
|
|
1690
|
-
if (
|
|
1691
|
-
res_read.has(pdu.DataAccessResult.READ_WRITE_DENIED, exc.ResultError)
|
|
1692
|
-
and not is_readable
|
|
1693
|
-
):
|
|
1694
|
-
c.log(logL.INFO, F"success ReadAccess TEST")
|
|
1695
|
-
else:
|
|
1696
|
-
res.append_err(res_read.err)
|
|
1697
|
-
elif not is_readable:
|
|
1698
|
-
res.append_e(PermissionError(f"{obj} with attr={i} must be unreadable"))
|
|
1699
|
-
indexes.remove(i)
|
|
1700
|
-
return res.result
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
@dataclass
|
|
1704
|
-
class ApplyTemplate(SimpleCopy, Base):
|
|
1705
|
-
template: collection.Template
|
|
1706
|
-
msg: str = "apply template"
|
|
1707
|
-
|
|
1708
|
-
async def exchange(self, c: Client) -> result.Result:
|
|
1709
|
-
# todo: search col
|
|
1710
|
-
attr: cdt.CommonDataType
|
|
1711
|
-
res = result.StrictOk()
|
|
1712
|
-
for col in self.template.collections:
|
|
1713
|
-
if col == c.objects:
|
|
1714
|
-
use_col = col
|
|
1715
|
-
break
|
|
1716
|
-
else:
|
|
1717
|
-
c.log(logL.ERR, F"not find collection for {c}")
|
|
1718
|
-
raise asyncio.CancelledError()
|
|
1719
|
-
for ln, indexes in self.template.used.items():
|
|
1720
|
-
if (obj := res.propagate_err(use_col.logicalName2obj(ln))) is not None:
|
|
1721
|
-
for i in indexes:
|
|
1722
|
-
if (attr := obj.get_attr(i)) is not None:
|
|
1723
|
-
res.propagate_err(await WriteAttribute(
|
|
1724
|
-
ln=ln,
|
|
1725
|
-
index=i,
|
|
1726
|
-
value=attr.encoding
|
|
1727
|
-
).exchange(c))
|
|
1728
|
-
else:
|
|
1729
|
-
res.append_e(exc.EmptyObj(F"skip apply {self.template} {ln}:{i}: no value"))
|
|
1730
|
-
return res
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
@dataclass
|
|
1734
|
-
class ReadTemplate(OK):
|
|
1735
|
-
template: collection.Template
|
|
1736
|
-
msg: str = "read template"
|
|
1737
|
-
|
|
1738
|
-
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1739
|
-
# todo: copypast from <ApplyTemplate>
|
|
1740
|
-
attr: cdt.CommonDataType
|
|
1741
|
-
res = result.ErrorAccumulator()
|
|
1742
|
-
for col in self.template.collections:
|
|
1743
|
-
if col == c._objects:
|
|
1744
|
-
use_col = col
|
|
1745
|
-
break
|
|
1746
|
-
else:
|
|
1747
|
-
c.log(logL.ERR, F"not find collection for {c}")
|
|
1748
|
-
raise asyncio.CancelledError()
|
|
1749
|
-
for ln, indexes in self.template.used.items():
|
|
1750
|
-
try:
|
|
1751
|
-
obj = use_col.get_object(ln) # todo: maybe not need
|
|
1752
|
-
except exc.NoObject as e:
|
|
1753
|
-
c.log(logL.WARN, F"skip apply {self.template}: {e}")
|
|
1754
|
-
continue
|
|
1755
|
-
res.propagate_err(
|
|
1756
|
-
await ReadAttributes(
|
|
1757
|
-
ln=ln,
|
|
1758
|
-
indexes=tuple(indexes)
|
|
1759
|
-
).exchange(c))
|
|
1760
|
-
return res.result
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
@dataclass
|
|
1764
|
-
class AccessValidate(OK):
|
|
1765
|
-
"""check all access rights for current SAP"""
|
|
1766
|
-
with_correct: bool = False
|
|
1767
|
-
msg: str = "all access validate"
|
|
1768
|
-
|
|
1769
|
-
# todo: make with result.Error
|
|
1770
|
-
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1771
|
-
res = result.ErrorAccumulator()
|
|
1772
|
-
obj_l: ObjectListType
|
|
1773
|
-
el: ObjectListElement
|
|
1774
|
-
a_a_i: association_ln.abstract.AttributeAccessItem
|
|
1775
|
-
if (obj_l := c.objects.sap2association(c.SAP).object_list) is None:
|
|
1776
|
-
return result.Error.from_e(exc.EmptyObj(F"empty object_list for {c._objects.sap2association(c.SAP)}"))
|
|
1777
|
-
for el in obj_l:
|
|
1778
|
-
for a_a_i in el.access_rights.attribute_access:
|
|
1779
|
-
if a_a_i.access_mode.is_readable():
|
|
1780
|
-
i = int(a_a_i.attribute_id)
|
|
1781
|
-
if isinstance(res_read :=await ReadByDescriptor(ut.CosemAttributeDescriptor((
|
|
1782
|
-
int(el.class_id),
|
|
1783
|
-
el.logical_name.contents,
|
|
1784
|
-
i
|
|
1785
|
-
))).exchange(c), result.Error):
|
|
1786
|
-
res.append_err(res_read.err)
|
|
1787
|
-
if self.with_correct:
|
|
1788
|
-
a_a_i.access_mode.set(1) # todo: make better in future
|
|
1789
|
-
elif a_a_i.access_mode.is_writable():
|
|
1790
|
-
if isinstance(res_write :=await WriteAttribute(
|
|
1791
|
-
ln=el.logical_name,
|
|
1792
|
-
index=i,
|
|
1793
|
-
value=res_read.value
|
|
1794
|
-
).exchange(c), result.Error):
|
|
1795
|
-
res.append_err(res_write.err)
|
|
1796
|
-
return res.result
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
@dataclass
|
|
1800
|
-
@deprecated("use <WriteList>")
|
|
1801
|
-
class WriteParDatas(SimpleCopy, _List[result.Ok]):
|
|
1802
|
-
"""write by ParData list"""
|
|
1803
|
-
par_datas: list[ParData]
|
|
1804
|
-
msg: str = ""
|
|
1805
|
-
|
|
1806
|
-
async def exchange(self, c: Client) -> result.List[result.Ok] | result.Error:
|
|
1807
|
-
res = result.List()
|
|
1808
|
-
for pardata in self.par_datas:
|
|
1809
|
-
res.append(await WriteAttribute(
|
|
1810
|
-
ln=pardata.par.ln,
|
|
1811
|
-
index=pardata.par.i,
|
|
1812
|
-
value=pardata.data.encoding
|
|
1813
|
-
).exchange(c))
|
|
1814
|
-
return res
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
class WriteList(SimpleCopy, _List[result.Ok]):
|
|
1818
|
-
"""write by list"""
|
|
1819
|
-
par_datas: tuple[tuple[Parameter, cdt.CommonDataType], ...]
|
|
1820
|
-
err_ignore: bool
|
|
1821
|
-
|
|
1822
|
-
def __init__(self, *par_datas: tuple[Parameter, cdt.CommonDataType], err_ignore: bool = False, msg: str = "write list") -> None:
|
|
1823
|
-
self.par_datas = par_datas
|
|
1824
|
-
self.err_ignore = err_ignore
|
|
1825
|
-
self.msg = msg
|
|
1826
|
-
|
|
1827
|
-
async def exchange(self, c: Client) -> result.List[result.Ok] | result.Error:
|
|
1828
|
-
res = result.List[result.Ok]()
|
|
1829
|
-
for par, data in self.par_datas:
|
|
1830
|
-
if (
|
|
1831
|
-
isinstance(res_one := await Write2(par, data).exchange(c), result.Error)
|
|
1832
|
-
and not self.err_ignore
|
|
1833
|
-
):
|
|
1834
|
-
return res_one
|
|
1835
|
-
res.append(res_one)
|
|
1836
|
-
return res
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
@dataclass(frozen=True)
|
|
1840
|
-
class WriteTranscript(SimpleCopy, OK):
|
|
1841
|
-
"""write by ParValues[Transcript]"""
|
|
1842
|
-
par: Parameter
|
|
1843
|
-
value: cdt.Transcript
|
|
1844
|
-
msg: str = "write with transcript"
|
|
1845
|
-
|
|
1846
|
-
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1847
|
-
if isinstance((res := await Par2Data[cdt.CommonDataType](self.par).exchange(c)), result.Error):
|
|
1848
|
-
return res
|
|
1849
|
-
if isinstance(res.value, cdt.Digital):
|
|
1850
|
-
s_u = c.objects.par2su(self.par)
|
|
1851
|
-
if isinstance(s_u, cdt.ScalUnitType):
|
|
1852
|
-
if not isinstance(self.value, str):
|
|
1853
|
-
return result.Error.from_e(TypeError(), f"for {self.par} got type: {self.value}, expected String")
|
|
1854
|
-
try:
|
|
1855
|
-
data = res.value.parse(value := str(float(self.value) * 10 ** -int(s_u.scaler)))
|
|
1856
|
-
except ValueError as e:
|
|
1857
|
-
return result.Error.from_e(e, f"for {self.par} got value: {self.value}, expected Float or Digital")
|
|
1858
|
-
else:
|
|
1859
|
-
data = res.value.parse(self.value)
|
|
1860
|
-
else:
|
|
1861
|
-
data = res.value.parse(self.value)
|
|
1862
|
-
return await Write2(self.par, data).exchange(c)
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
class WriteTranscripts(SimpleCopy, _List[result.Ok]):
|
|
1866
|
-
"""write by ParValues[Transcript] list"""
|
|
1867
|
-
par_values: tuple[tuple[Parameter, cdt.Transcript], ...]
|
|
1868
|
-
err_ignore: bool
|
|
1869
|
-
|
|
1870
|
-
def __init__(self, *par_values: tuple[Parameter, cdt.Transcript], err_ignore: bool = False, msg: str ="write transcripts"):
|
|
1871
|
-
self.par_values = par_values
|
|
1872
|
-
self.err_ignore = err_ignore
|
|
1873
|
-
self.msg = msg
|
|
1874
|
-
|
|
1875
|
-
async def exchange(self, c: Client) -> result.List[result.Ok] | result.Error:
|
|
1876
|
-
res = result.List[result.Ok]()
|
|
1877
|
-
for par, value in self.par_values:
|
|
1878
|
-
if (
|
|
1879
|
-
isinstance(res_one := await WriteTranscript(par, value).exchange(c), result.Error)
|
|
1880
|
-
and not self.err_ignore
|
|
1881
|
-
):
|
|
1882
|
-
return res_one
|
|
1883
|
-
res.append(res_one)
|
|
1884
|
-
return res
|
|
1
|
+
import asyncio
|
|
2
|
+
import random
|
|
3
|
+
from copy import copy
|
|
4
|
+
import re
|
|
5
|
+
from typing_extensions import deprecated
|
|
6
|
+
import hashlib
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Callable, Any, Optional, Protocol, cast, override, Self, Final, Iterable, TypeVarTuple
|
|
9
|
+
from itertools import count
|
|
10
|
+
import datetime
|
|
11
|
+
import time
|
|
12
|
+
from semver import Version as SemVer
|
|
13
|
+
from StructResult import result
|
|
14
|
+
from DLMS_SPODES.pardata import ParValues
|
|
15
|
+
from DLMS_SPODES.types.implementations import enums, long_unsigneds, bitstrings, octet_string, structs, arrays, integers
|
|
16
|
+
from DLMS_SPODES.pardata import ParData
|
|
17
|
+
from DLMS_SPODES.cosem_interface_classes import parameters as dlms_par
|
|
18
|
+
from DLMS_SPODES.cosem_interface_classes.parameter import Parameter
|
|
19
|
+
from DLMS_SPODES import exceptions as exc, pdu_enums as pdu
|
|
20
|
+
from DLMS_SPODES.cosem_interface_classes import (
|
|
21
|
+
cosem_interface_class as ic,
|
|
22
|
+
collection,
|
|
23
|
+
overview,
|
|
24
|
+
ln_pattern
|
|
25
|
+
)
|
|
26
|
+
from DLMS_SPODES.cosem_interface_classes.clock import Clock
|
|
27
|
+
from DLMS_SPODES.cosem_interface_classes.image_transfer.image_transfer_status import ImageTransferStatus
|
|
28
|
+
from DLMS_SPODES.cosem_interface_classes.image_transfer.ver0 import ImageTransferInitiate, ImageBlockTransfer, ImageToActivateInfo
|
|
29
|
+
from DLMS_SPODES.cosem_interface_classes.association_ln.ver0 import ObjectListType, ObjectListElement
|
|
30
|
+
from DLMS_SPODES.cosem_interface_classes import association_ln
|
|
31
|
+
from DLMS_SPODES.types import cdt, ut, cst
|
|
32
|
+
from DLMS_SPODES.hdlc import frame
|
|
33
|
+
from DLMS_SPODES.enums import Transmit, Application, Conformance
|
|
34
|
+
from DLMS_SPODES.firmwares import get_firmware
|
|
35
|
+
from DLMS_SPODES.cosem_interface_classes.image_transfer import image_transfer_status as i_t_status
|
|
36
|
+
from DLMSAdapter.main import AdapterException, Adapter, gag
|
|
37
|
+
from DLMSCommunicationProfile.osi import OSI
|
|
38
|
+
from .logger import LogLevel as logL
|
|
39
|
+
from .client import Client, Security, Data, mechanism_id, AcseServiceUser, State
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
firm_id_pat = re.compile(b".*(?P<fid>PWRM_M2M_[^_]{1,10}_[^_]{1,10}).+")
|
|
43
|
+
boot_ver_pat = re.compile(b"(?P<boot_ver>\\d{1,4}).+")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
type Errors = list[Exception]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Base[T: result.Result](Protocol):
|
|
50
|
+
"""Exchange task for DLMS client"""
|
|
51
|
+
msg: str
|
|
52
|
+
|
|
53
|
+
def copy(self) -> Self: ...
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def current(self) -> 'Base[T] | Self':
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
async def run(self, c: Client) -> T | result.Error:
|
|
60
|
+
"""exception handling block"""
|
|
61
|
+
try:
|
|
62
|
+
return await self.physical(c)
|
|
63
|
+
except (ConnectionRefusedError, TimeoutError) as e:
|
|
64
|
+
return result.Error.from_e(e)
|
|
65
|
+
except exc.DLMSException as e:
|
|
66
|
+
return result.Error.from_e(e)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
return result.Error.from_e(e)
|
|
69
|
+
# except asyncio.CancelledError as e:
|
|
70
|
+
# await c.close() # todo: change to DiscRequest
|
|
71
|
+
# return result.Error.from_e(exc.Abort("manual stop")) # for handle BaseException
|
|
72
|
+
finally:
|
|
73
|
+
c.received_frames.clear() # for next exchange need clear all received frames. todo: this bag, remove in future
|
|
74
|
+
|
|
75
|
+
async def PH_connect(self, c: Client) -> result.Ok | result.Error:
|
|
76
|
+
if c.media is None:
|
|
77
|
+
return result.Error.from_e(exc.NoPort("no media"), "PH_connect")
|
|
78
|
+
if not c.media.is_open():
|
|
79
|
+
if isinstance(res_open := await c.media.open(), result.Error):
|
|
80
|
+
return res_open
|
|
81
|
+
c.log(logL.INFO, F"Open port communication channel: {c.media} {res_open.value}sec")
|
|
82
|
+
c.level = OSI.PHYSICAL
|
|
83
|
+
# todo: replace to <data_link>
|
|
84
|
+
if (
|
|
85
|
+
c._objects is None
|
|
86
|
+
and not isinstance(self, InitType)
|
|
87
|
+
):
|
|
88
|
+
if isinstance(res := await init_type.data_link(c), result.Error):
|
|
89
|
+
return res.with_msg("PH_connect")
|
|
90
|
+
if isinstance(res_close := await c.close(), result.Error): # todo: change to DiscRequest, or make not closed or reconnect !!!
|
|
91
|
+
return res_close
|
|
92
|
+
return result.OK
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
async def physical_t(c: Client) -> result.Ok | result.Error:
|
|
96
|
+
return await c.close()
|
|
97
|
+
|
|
98
|
+
async def physical(self, c: Client) -> T | result.Error:
|
|
99
|
+
if OSI.PHYSICAL not in c.level:
|
|
100
|
+
if isinstance((res := await self.PH_connect(c)), result.Error):
|
|
101
|
+
return res
|
|
102
|
+
ret = await self.data_link(c)
|
|
103
|
+
if isinstance(res_terminate := await self.physical_t(c), result.Error):
|
|
104
|
+
return res_terminate
|
|
105
|
+
return ret
|
|
106
|
+
|
|
107
|
+
async def DL_connect(self, c: Client) -> result.Ok | result.Error:
|
|
108
|
+
"""Data link Layer connect"""
|
|
109
|
+
c.send_frames.clear()
|
|
110
|
+
# calculate addresses todo: move to c.com_profile(HDLC)
|
|
111
|
+
c.DA = frame.Address(
|
|
112
|
+
upper_address=int(c.server_SAP),
|
|
113
|
+
lower_address=c.com_profile.parameters.device_address,
|
|
114
|
+
length=c.addr_size
|
|
115
|
+
)
|
|
116
|
+
c.SA = frame.Address(upper_address=int(c.SAP))
|
|
117
|
+
c.log(logL.INFO, F"{c.SA=} {c.DA=}")
|
|
118
|
+
# initialize connection
|
|
119
|
+
if c.settings.cipher.security != Security.NONE:
|
|
120
|
+
c.log(logL.DEB, F"Security: {c.settings.cipher.security}/n"
|
|
121
|
+
F"System title: {c.settings.cipher.systemTitle.hex()}"
|
|
122
|
+
F"Authentication key: {c.settings.cipher.authenticationKey.hex()}"
|
|
123
|
+
F"Block cipher key: {c.settings.cipher.blockCipherKey.hex()}")
|
|
124
|
+
if c.settings.cipher.dedicatedKey:
|
|
125
|
+
c.log(logL.DEB, F"Dedicated key: {c.settings.cipher.dedicatedKey.hex()}")
|
|
126
|
+
# SNRM
|
|
127
|
+
c.get_SNRM_request()
|
|
128
|
+
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
129
|
+
return res_pdu
|
|
130
|
+
c.level |= OSI.DATA_LINK
|
|
131
|
+
return result.OK
|
|
132
|
+
|
|
133
|
+
async def data_link(self, c: Client) -> T | result.Error:
|
|
134
|
+
if OSI.DATA_LINK not in c.level:
|
|
135
|
+
if isinstance(res_conn := await self.DL_connect(c), result.Error):
|
|
136
|
+
return res_conn
|
|
137
|
+
# todo: make tile
|
|
138
|
+
return await self.application(c)
|
|
139
|
+
|
|
140
|
+
async def AA(self, c: Client) -> result.Ok | result.Error:
|
|
141
|
+
"""Application Associate"""
|
|
142
|
+
if c.invocationCounter and c.settings.cipher is not None and c.settings.cipher.security != Security.NONE:
|
|
143
|
+
# create IC object. TODO: remove it after close connection, maybe???
|
|
144
|
+
c.settings.proposedConformance |= Conformance.GENERAL_PROTECTION
|
|
145
|
+
|
|
146
|
+
# my block
|
|
147
|
+
IC: Data = c.objects.add_if_missing(ut.CosemClassId(1),
|
|
148
|
+
logical_name=cst.LogicalName(bytearray((0, c.get_channel_index(), 43, 1,
|
|
149
|
+
c.current_association.security_setup_reference.e, 255))),
|
|
150
|
+
version=cdt.Unsigned(0))
|
|
151
|
+
tmp_client_SAP = c.current_association.associated_partners_id.client_SAP
|
|
152
|
+
challenge = c.settings.ctoSChallenge
|
|
153
|
+
try:
|
|
154
|
+
c.aarqRequest(c.m_id)
|
|
155
|
+
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
156
|
+
return res_pdu
|
|
157
|
+
ret = c.parseAareResponse(res_pdu.value)
|
|
158
|
+
c.level |= OSI.APPLICATION # todo: it's must be result of <ret> look down
|
|
159
|
+
if isinstance(res_ic := await ReadObjAttr(IC, 2).exchange(c), result.Error):
|
|
160
|
+
return res_ic
|
|
161
|
+
c.settings.cipher.invocationCounter = 1 + int(res_ic.value)
|
|
162
|
+
c.log(logL.DEB, "Invocation counter: " + str(c.settings.cipher.invocationCounter))
|
|
163
|
+
# disconnect
|
|
164
|
+
if c.media and c.media.is_open():
|
|
165
|
+
c.log(logL.DEB, "DisconnectRequest")
|
|
166
|
+
if isinstance(res_disc_req := await c.disconnect_request(), result.Error):
|
|
167
|
+
return res_disc_req
|
|
168
|
+
finally:
|
|
169
|
+
c.SAP = tmp_client_SAP
|
|
170
|
+
c.settings.useCustomChallenge = challenge is not None
|
|
171
|
+
c.settings.ctoSChallenge = challenge
|
|
172
|
+
|
|
173
|
+
# gurux with several removed methods
|
|
174
|
+
# add = self.settings.clientAddress
|
|
175
|
+
# auth = self.settings.authentication
|
|
176
|
+
# security = self.client.ciphering.security
|
|
177
|
+
# challenge = self.client.ctoSChallenge
|
|
178
|
+
# try:
|
|
179
|
+
# self.client.clientAddress = 16
|
|
180
|
+
# self.settings.authentication = Authentication.NONE
|
|
181
|
+
# self.client.ciphering.security = Security.NONE
|
|
182
|
+
# reply = GXReplyData()
|
|
183
|
+
# self.get_SNRM_request()
|
|
184
|
+
# self.status = Status.READ
|
|
185
|
+
# self.read_data_block2()
|
|
186
|
+
# self.objects.IEC_HDLS_setup.set_from_info(self.reply.data.get_data())
|
|
187
|
+
# self.connection_state = ConnectionState.HDLC
|
|
188
|
+
# self.reply.clear()
|
|
189
|
+
# self.aarqRequest()
|
|
190
|
+
# self.read_data_block2()
|
|
191
|
+
# self.parseAareResponse(reply.data)
|
|
192
|
+
# reply.clear()
|
|
193
|
+
# item = GXDLMSData(self.invocationCounter)
|
|
194
|
+
# data = self.client.read(item, 2)[0]
|
|
195
|
+
# reply = GXReplyData()
|
|
196
|
+
# self.read_data_block(data, reply)
|
|
197
|
+
# item.encodings[2] = reply.data.get_data()
|
|
198
|
+
# Update data type on read.
|
|
199
|
+
# if item.getDataType(2) == cdt.NullData.TAG:
|
|
200
|
+
# item.setDataType(2, reply.valueType)
|
|
201
|
+
# self.client.updateValue(item, 2, reply.value)
|
|
202
|
+
# self.client.ciphering.invocationCounter = 1 + item.value
|
|
203
|
+
# print("Invocation counter: " + str(self.client.ciphering.invocationCounter))
|
|
204
|
+
# if self.media and self.media.isOpen():
|
|
205
|
+
# self.log(logL.INFO, "DisconnectRequest")
|
|
206
|
+
# self.disconnect_request()
|
|
207
|
+
# finally:
|
|
208
|
+
# self.settings.clientAddress = add
|
|
209
|
+
# self.settings.authentication = auth
|
|
210
|
+
# self.client.ciphering.security = security
|
|
211
|
+
# self.client.ctoSChallenge = challenge
|
|
212
|
+
|
|
213
|
+
c.aarqRequest(c.m_id)
|
|
214
|
+
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
215
|
+
return res_pdu
|
|
216
|
+
# await c.read_attr(ut.CosemAttributeDescriptor((collection.ClassID.ASSOCIATION_LN, ut.CosemObjectInstanceId("0.0.40.0.0.255"), ut.CosemObjectAttributeId(6)))) # for test only
|
|
217
|
+
try:
|
|
218
|
+
parse = c.parseAareResponse(res_pdu.value)
|
|
219
|
+
except IndexError as e:
|
|
220
|
+
print(e)
|
|
221
|
+
match parse:
|
|
222
|
+
case AcseServiceUser.NULL:
|
|
223
|
+
c.log(logL.INFO, "Authentication success")
|
|
224
|
+
c.level |= OSI.APPLICATION
|
|
225
|
+
case AcseServiceUser.AUTHENTICATION_REQUIRED:
|
|
226
|
+
c.getApplicationAssociationRequest()
|
|
227
|
+
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
228
|
+
return res_pdu
|
|
229
|
+
c.parseApplicationAssociationResponse(res_pdu.value)
|
|
230
|
+
case _ as diagnostic:
|
|
231
|
+
return result.Error.from_e(exc.AssociationResultError(diagnostic))
|
|
232
|
+
if c._objects is not None:
|
|
233
|
+
matchLDN = change_ldn if c.is_universal() else match_ldn
|
|
234
|
+
if isinstance(res_match_ldn := await matchLDN.exchange(c), result.Error):
|
|
235
|
+
return res_match_ldn
|
|
236
|
+
return result.OK
|
|
237
|
+
|
|
238
|
+
async def application(self, c: Client) -> T | result.Error:
|
|
239
|
+
if OSI.APPLICATION not in c.level:
|
|
240
|
+
if isinstance(res := await self.AA(c), result.Error):
|
|
241
|
+
return res
|
|
242
|
+
# no tile
|
|
243
|
+
return await self.exchange(c)
|
|
244
|
+
|
|
245
|
+
async def exchange(self, c: Client) -> T | result.Error:
|
|
246
|
+
"""application level exchange"""
|
|
247
|
+
|
|
248
|
+
async def connect(self, c: Client) -> result.Ok | result.Error:
|
|
249
|
+
await self.PH_connect(c)
|
|
250
|
+
await self.DL_connect(c)
|
|
251
|
+
return await self.AA(c)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class SimpleCopy:
|
|
255
|
+
def copy(self) -> Self:
|
|
256
|
+
return self
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class Simple[T](Base[result.Simple[T]], Protocol):
|
|
260
|
+
"""Simple result"""
|
|
261
|
+
@override
|
|
262
|
+
async def exchange(self, c: Client) -> result.SimpleOrError[T]: ...
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class Boolean(Simple[bool], Protocol):
|
|
266
|
+
"""Simple[bool] result"""
|
|
267
|
+
@override
|
|
268
|
+
async def exchange(self, c: Client) -> result.SimpleOrError[bool]: ...
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class CDT[T: cdt.CommonDataType](Simple[T], Protocol):
|
|
272
|
+
"""Simple[CDT] result"""
|
|
273
|
+
@override
|
|
274
|
+
async def exchange(self, c: Client) -> result.SimpleOrError[T]: ...
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class _List[T](Base[result.List[T]], Protocol):
|
|
278
|
+
"""With List result"""
|
|
279
|
+
@override
|
|
280
|
+
async def exchange(self, c: Client) -> result.List[T] | result.Error: ...
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class _Sequence[*Ts](Base[result.Sequence[*Ts]], Protocol):
|
|
284
|
+
"""With List result"""
|
|
285
|
+
@override
|
|
286
|
+
async def exchange(self, c: Client) -> result.Sequence[*Ts] | result.Error: ...
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class OK(Base[result.Ok], Protocol):
|
|
290
|
+
"""Always result OK"""
|
|
291
|
+
|
|
292
|
+
@override
|
|
293
|
+
async def exchange(self, c: Client) -> result.Ok | result.Error: ...
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class StrictOK(Base[result.StrictOk], Protocol):
|
|
297
|
+
"""Always result OK"""
|
|
298
|
+
|
|
299
|
+
@override
|
|
300
|
+
async def exchange(self, c: Client) -> result.StrictOk | result.Error: ...
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@dataclass(frozen=True)
|
|
304
|
+
class ClientBlocking(SimpleCopy, OK):
|
|
305
|
+
"""complete by time or abort"""
|
|
306
|
+
delay: float = field(default=99999999.0)
|
|
307
|
+
msg: str = "client blocking"
|
|
308
|
+
|
|
309
|
+
async def run(self, c: Client) -> result.Ok | result.Error:
|
|
310
|
+
try:
|
|
311
|
+
c.level = OSI.APPLICATION
|
|
312
|
+
c.log(logL.WARN, F"blocked for {self.delay} second")
|
|
313
|
+
await asyncio.sleep(self.delay)
|
|
314
|
+
return result.OK
|
|
315
|
+
finally:
|
|
316
|
+
c.level = OSI.NONE
|
|
317
|
+
return result.OK
|
|
318
|
+
|
|
319
|
+
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
320
|
+
raise RuntimeError(f"not support for {self.__class__.__name__}")
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# todo: make with <data_link>
|
|
324
|
+
@dataclass
|
|
325
|
+
class TestDataLink(SimpleCopy, OK):
|
|
326
|
+
msg: str = "test DLink"
|
|
327
|
+
|
|
328
|
+
async def physical(self, c: Client) -> result.Ok | result.Error:
|
|
329
|
+
if OSI.PHYSICAL not in c.level:
|
|
330
|
+
if not c.media.is_open():
|
|
331
|
+
if isinstance(res_open := await c.media.open(), result.Error):
|
|
332
|
+
return res_open
|
|
333
|
+
c.level = OSI.PHYSICAL
|
|
334
|
+
c.DA = frame.Address(
|
|
335
|
+
upper_address=int(c.server_SAP),
|
|
336
|
+
lower_address=c.com_profile.parameters.device_address,
|
|
337
|
+
length=c.addr_size
|
|
338
|
+
)
|
|
339
|
+
c.SA = frame.Address(upper_address=int(c.SAP))
|
|
340
|
+
c.get_SNRM_request()
|
|
341
|
+
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
342
|
+
return res_pdu
|
|
343
|
+
c.level |= OSI.DATA_LINK
|
|
344
|
+
if isinstance(res_close := await c.close(), result.Error): # todo: change to DiscRequest
|
|
345
|
+
return res_close
|
|
346
|
+
return result.Ok
|
|
347
|
+
|
|
348
|
+
async def exchange(self, c: Client):
|
|
349
|
+
return result.OK
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@dataclass(frozen=True)
|
|
353
|
+
class Dummy(SimpleCopy, OK):
|
|
354
|
+
msg: str = "dummy"
|
|
355
|
+
|
|
356
|
+
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
357
|
+
""""""
|
|
358
|
+
return result.OK
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@dataclass(frozen=True)
|
|
362
|
+
class HardwareDisconnect(SimpleCopy, OK):
|
|
363
|
+
msg: str = "hardware disconnect"
|
|
364
|
+
|
|
365
|
+
@override
|
|
366
|
+
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
367
|
+
await c.media.close()
|
|
368
|
+
c.level = OSI.NONE
|
|
369
|
+
msg = '' if self.msg is None else F": {self.msg}"
|
|
370
|
+
c.log(logL.WARN, F"HARDWARE DISCONNECT{msg}")
|
|
371
|
+
return result.OK
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@dataclass(frozen=True)
|
|
375
|
+
class HardwareReconnect(SimpleCopy, OK):
|
|
376
|
+
delay: float = 0.0
|
|
377
|
+
"""delay between disconnect and restore Application"""
|
|
378
|
+
msg: str = "reconnect media without response"
|
|
379
|
+
|
|
380
|
+
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
381
|
+
if isinstance((res := await HardwareDisconnect().exchange(c)), result.Error):
|
|
382
|
+
return res
|
|
383
|
+
if self.delay != 0.0:
|
|
384
|
+
c.log(logL.INFO, F"delay({self.delay})")
|
|
385
|
+
await asyncio.sleep(self.delay)
|
|
386
|
+
if isinstance(res_connect := await self.connect(c), result.Error): # restore Application
|
|
387
|
+
return res_connect
|
|
388
|
+
return result.OK
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
@dataclass(frozen=True)
|
|
392
|
+
class Loop(OK):
|
|
393
|
+
task: Base[result.Result]
|
|
394
|
+
func: Callable[[Any], bool]
|
|
395
|
+
delay: int = 0.0
|
|
396
|
+
msg: str = "loop"
|
|
397
|
+
attempt_amount: int = 0
|
|
398
|
+
"""0 is never end loop"""
|
|
399
|
+
|
|
400
|
+
def copy(self) -> Self:
|
|
401
|
+
return Loop(
|
|
402
|
+
task=self.task.copy(),
|
|
403
|
+
func=self.func,
|
|
404
|
+
delay=self.delay,
|
|
405
|
+
msg=self.msg,
|
|
406
|
+
attempt_amount=self.attempt_amount
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
async def run(self, c: Client) -> result.Result:
|
|
410
|
+
attempt = count()
|
|
411
|
+
while not self.func(await super(Loop, self).run(c)):
|
|
412
|
+
if next(attempt) == self.attempt_amount:
|
|
413
|
+
return result.Error.from_e(ValueError("end of attempts"))
|
|
414
|
+
await asyncio.sleep(self.delay)
|
|
415
|
+
return result.OK
|
|
416
|
+
|
|
417
|
+
async def exchange(self, c: Client) -> result.Result:
|
|
418
|
+
return await self.task.exchange(c)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@dataclass
|
|
422
|
+
class ConditionalTask[T](Base):
|
|
423
|
+
"""Warning: experimental"""
|
|
424
|
+
precondition_task: Base[result.Result]
|
|
425
|
+
comp_value: T
|
|
426
|
+
predicate: Callable[[T, T], bool]
|
|
427
|
+
main_task: Base[result.Result]
|
|
428
|
+
msg: str = "conditional task"
|
|
429
|
+
|
|
430
|
+
async def exchange(self, c: Client) -> result.Result:
|
|
431
|
+
res = await self.precondition_task.exchange(c)
|
|
432
|
+
if self.predicate(self.comp_value, res.value):
|
|
433
|
+
return await self.main_task.exchange(c)
|
|
434
|
+
return result.Error.from_e(ValueError("Condition not satisfied"))
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
DEFAULT_DATETIME_SCHEDULER = cdt.DateTime.parse("1.1.0001")
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@dataclass
|
|
441
|
+
class Scheduler[T: result.Result](Base[T]):
|
|
442
|
+
"""𝑟𝑒𝑝𝑒𝑡𝑖𝑡𝑖𝑜𝑛_𝑑𝑒𝑙𝑎𝑦 = 𝑟𝑒𝑝𝑒𝑡𝑖𝑡𝑖𝑜𝑛_𝑑𝑒𝑙𝑎𝑦_𝑚𝑖𝑛 × (𝑟𝑒𝑝𝑒𝑡𝑖𝑡𝑖𝑜𝑛_𝑑𝑒𝑙𝑎𝑦_𝑒𝑥𝑝𝑜𝑛𝑒𝑛𝑡 × 0.01) ** 𝑛"""
|
|
443
|
+
task: Base[T]
|
|
444
|
+
execution_datetime: Final[cdt.DateTime] = DEFAULT_DATETIME_SCHEDULER
|
|
445
|
+
start_interval: Final[int] = 0
|
|
446
|
+
number_of_retries: Final[int] = 3
|
|
447
|
+
total_of_retries: Final[int] = 100
|
|
448
|
+
repetition_delay_min: Final[int] = 1
|
|
449
|
+
repetition_delay_exponent: Final[int] = 100
|
|
450
|
+
repetition_delay_max: Final[int] = 100
|
|
451
|
+
msg: str = "sheduler"
|
|
452
|
+
|
|
453
|
+
def copy(self) -> "Scheduler[T]":
|
|
454
|
+
return Scheduler(
|
|
455
|
+
task=self.task.copy(),
|
|
456
|
+
execution_datetime=self.execution_datetime,
|
|
457
|
+
start_interval=self.start_interval,
|
|
458
|
+
number_of_retries=self.number_of_retries,
|
|
459
|
+
total_of_retries=self.total_of_retries,
|
|
460
|
+
repetition_delay_min=self.repetition_delay_min,
|
|
461
|
+
repetition_delay_exponent=self.repetition_delay_exponent,
|
|
462
|
+
repetition_delay_max=self.repetition_delay_max,
|
|
463
|
+
msg=self.msg
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
async def run(self, c: Client) -> T | result.Error:
|
|
467
|
+
if self.start_interval != 0:
|
|
468
|
+
await asyncio.sleep(random.uniform(0, self.start_interval))
|
|
469
|
+
c.log(logL.INFO, f"start {self.__class__.__name__}")
|
|
470
|
+
total_of_retries = count()
|
|
471
|
+
is_start: bool = True
|
|
472
|
+
acc = result.ErrorAccumulator()
|
|
473
|
+
while True:
|
|
474
|
+
dt = self.execution_datetime.get_right_nearest_datetime(now := datetime.datetime.now())
|
|
475
|
+
if dt is None:
|
|
476
|
+
if is_start:
|
|
477
|
+
is_start = False
|
|
478
|
+
else:
|
|
479
|
+
return acc.as_error(exc.Timeout("start time is out"), msg=self.msg)
|
|
480
|
+
else:
|
|
481
|
+
delay = (dt - now).total_seconds()
|
|
482
|
+
c.log(logL.WARN, f"wait for {delay=}")
|
|
483
|
+
await asyncio.sleep(delay)
|
|
484
|
+
for n in range(self.number_of_retries):
|
|
485
|
+
if next(total_of_retries) > self.total_of_retries:
|
|
486
|
+
return acc.as_error(exc.Timeout("out of total retries"), msg=self.msg)
|
|
487
|
+
await asyncio.sleep(min(self.repetition_delay_max, self.repetition_delay_min*(self.repetition_delay_exponent * 0.01)**n))
|
|
488
|
+
if isinstance(res := await super(Scheduler, self).run(c), result.Error):
|
|
489
|
+
acc.append_err(res.err)
|
|
490
|
+
else:
|
|
491
|
+
return res
|
|
492
|
+
|
|
493
|
+
async def exchange(self, c: Client) -> T | result.Error:
|
|
494
|
+
return await self.task.exchange(c)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
@dataclass
|
|
498
|
+
class Subtasks[U: Base[result.Result]](Protocol):
|
|
499
|
+
"""for register longer other tasks into task"""
|
|
500
|
+
tasks: Iterable[U]
|
|
501
|
+
|
|
502
|
+
@property
|
|
503
|
+
def current(self) -> U | Self: ...
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
class List[T: result.Result, U: Base[result.Result]](Subtasks[U], _List[T]):
|
|
507
|
+
"""for exchange task sequence"""
|
|
508
|
+
__is_exchange: bool
|
|
509
|
+
err_ignore: bool
|
|
510
|
+
msg: str
|
|
511
|
+
__current: Base[T]
|
|
512
|
+
|
|
513
|
+
def __init__(self, *tasks: Base[T], msg: str = "", err_ignore: bool = False):
|
|
514
|
+
self.tasks = list(tasks)
|
|
515
|
+
self.__current = self
|
|
516
|
+
self.__is_exchange = False
|
|
517
|
+
self.msg = self.__class__.__name__ if msg == "" else msg
|
|
518
|
+
self.err_ignore = err_ignore
|
|
519
|
+
|
|
520
|
+
def copy(self) -> Self:
|
|
521
|
+
if all((isinstance(t, SimpleCopy) for t in self.tasks)):
|
|
522
|
+
return self
|
|
523
|
+
return List(
|
|
524
|
+
*(t.copy() for t in self.tasks),
|
|
525
|
+
msg=self.msg,
|
|
526
|
+
err_ignore=self.err_ignore
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
@property
|
|
530
|
+
def current(self) -> 'Base[T] | Self':
|
|
531
|
+
return self.__current
|
|
532
|
+
|
|
533
|
+
def append(self, task: Base[T]):
|
|
534
|
+
if not self.__is_exchange:
|
|
535
|
+
self.tasks.append(task)
|
|
536
|
+
else:
|
|
537
|
+
raise RuntimeError(F"append to {self.__class__.__name__} not allowed, already exchange started")
|
|
538
|
+
|
|
539
|
+
async def exchange(self, c: Client) -> result.List[T] | result.Error:
|
|
540
|
+
res = result.List()
|
|
541
|
+
self.__is_exchange = True
|
|
542
|
+
for t in self.tasks:
|
|
543
|
+
self.__current = t
|
|
544
|
+
if (
|
|
545
|
+
isinstance(res_one := await t.exchange(c), result.Error)
|
|
546
|
+
and not self.err_ignore
|
|
547
|
+
):
|
|
548
|
+
return res_one
|
|
549
|
+
res.append(res_one)
|
|
550
|
+
return res
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
class Sequence[*Ts](Subtasks[Base[result.Result]], _Sequence[*Ts]):
|
|
554
|
+
"""for exchange task sequence"""
|
|
555
|
+
msg: str
|
|
556
|
+
err_ignore: bool
|
|
557
|
+
__current: "Base[result.Result] | Sequence[*Ts]"
|
|
558
|
+
tasks: tuple[Base[result.Result], ...]
|
|
559
|
+
|
|
560
|
+
def __init__(self, *tasks: Base[result.Result], msg: str = "sequence", err_ignore: bool = False):
|
|
561
|
+
self.tasks = tasks
|
|
562
|
+
self.__current = self
|
|
563
|
+
self.msg = self.__class__.__name__ if msg == "" else msg
|
|
564
|
+
self.err_ignore = err_ignore
|
|
565
|
+
|
|
566
|
+
def copy(self) -> "Sequence[*Ts]":
|
|
567
|
+
if all((isinstance(t, SimpleCopy) for t in self.tasks)):
|
|
568
|
+
return self
|
|
569
|
+
return Sequence(
|
|
570
|
+
*(t.copy() for t in self.tasks),
|
|
571
|
+
msg=self.msg,
|
|
572
|
+
err_ignore=self.err_ignore
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
@property
|
|
576
|
+
def current(self) -> Base[result.Result] | Self:
|
|
577
|
+
return self.__current
|
|
578
|
+
|
|
579
|
+
async def exchange(self, c: Client) -> result.Sequence[*Ts] | result.Error:
|
|
580
|
+
res = result.Sequence()
|
|
581
|
+
for t in self.tasks:
|
|
582
|
+
self.__current = t
|
|
583
|
+
if isinstance(res_one := await t.exchange(c), result.Error):
|
|
584
|
+
if self.err_ignore:
|
|
585
|
+
res_one = res_one.with_msg(self.msg)
|
|
586
|
+
else:
|
|
587
|
+
return res_one
|
|
588
|
+
res = res.add(res_one)
|
|
589
|
+
return cast("result.Sequence[*Ts]", res)
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
@dataclass(frozen=True)
|
|
593
|
+
class SetLocalTime(SimpleCopy, OK):
|
|
594
|
+
"""without decide time transfer"""
|
|
595
|
+
msg: str = "set local time"
|
|
596
|
+
|
|
597
|
+
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
598
|
+
clock_obj: Clock = c.objects.get_object("0.0.1.0.0.255")
|
|
599
|
+
if isinstance(res := await ReadAttribute(
|
|
600
|
+
ln=clock_obj.logical_name,
|
|
601
|
+
index=3
|
|
602
|
+
).exchange(c), result.Error):
|
|
603
|
+
return ret
|
|
604
|
+
delta = datetime.timedelta(minutes=int(res.value))
|
|
605
|
+
dt = cst.OctetStringDateTime(datetime.datetime.now(datetime.UTC)+delta)
|
|
606
|
+
if isinstance(res := await WriteAttribute(
|
|
607
|
+
ln=clock_obj.logical_name,
|
|
608
|
+
index=2,
|
|
609
|
+
value=dt.encoding
|
|
610
|
+
).exchange(c), result.Error):
|
|
611
|
+
return res
|
|
612
|
+
return result.OK
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
@dataclass(frozen=True)
|
|
616
|
+
class GetFirmwareVersion(SimpleCopy, CDT):
|
|
617
|
+
msg: str = "get firmware version"
|
|
618
|
+
|
|
619
|
+
async def exchange(self, c: Client) -> result.SimpleOrError[cdt.CommonDataType]:
|
|
620
|
+
return await Par2Data(Parameter(c.objects.id.f_ver.par[:6]).get_attr(c.objects.id.f_ver.par[6])).exchange(c)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
@dataclass(frozen=True)
|
|
624
|
+
class ReadByDescriptor(SimpleCopy, Simple[bytes]):
|
|
625
|
+
desc: ut.CosemMethodDescriptor
|
|
626
|
+
msg: str = "get encoding by Cosem-Attribute-Descriptor"
|
|
627
|
+
|
|
628
|
+
async def exchange(self, c: Client) -> result.SimpleOrError[bytes]:
|
|
629
|
+
c.get_get_request_normal(self.desc)
|
|
630
|
+
return await c.read_data_block()
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
@dataclass(frozen=True)
|
|
634
|
+
class FindFirmwareVersion(SimpleCopy, Simple[collection.ParameterValue]):
|
|
635
|
+
msg: str = "try find COSEM server version, return: instance(B group), CDT"
|
|
636
|
+
|
|
637
|
+
async def exchange(self, c: Client) -> result.SimpleOrError[collection.ParameterValue]:
|
|
638
|
+
err = result.ErrorAccumulator()
|
|
639
|
+
for desc in (ut.CosemAttributeDescriptor((1, "0.0.0.2.1.255", 2)), ut.CosemAttributeDescriptor((1, "0.0.96.1.2.255", 2))):
|
|
640
|
+
if isinstance(res_read := await ReadByDescriptor(desc).exchange(c), result.Error):
|
|
641
|
+
err.append_err(res_read.err)
|
|
642
|
+
in_e, out_e = res_read.err.split(exc.ResultError)
|
|
643
|
+
if (
|
|
644
|
+
out_e is None
|
|
645
|
+
and in_e.exceptions[0].args[0] == pdu.DataAccessResult(4)
|
|
646
|
+
):
|
|
647
|
+
continue
|
|
648
|
+
else:
|
|
649
|
+
return res_read
|
|
650
|
+
else:
|
|
651
|
+
res = result.Simple(collection.ParameterValue(
|
|
652
|
+
par=desc.instance_id.contents + desc.attribute_id.contents,
|
|
653
|
+
value=res_read.value
|
|
654
|
+
))
|
|
655
|
+
res.propagate_err(err)
|
|
656
|
+
return res
|
|
657
|
+
return err.as_error()
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
@dataclass(frozen=True)
|
|
661
|
+
class FindFirmwareId(SimpleCopy, Simple[collection.ParameterValue]):
|
|
662
|
+
msg: str = "find firmaware Identifier"
|
|
663
|
+
|
|
664
|
+
async def exchange(self, c: Client) -> result.SimpleOrError[collection.ParameterValue]:
|
|
665
|
+
err = result.ErrorAccumulator()
|
|
666
|
+
for desc in (ut.CosemAttributeDescriptor((1, "0.0.0.2.0.255", 2)), ut.CosemAttributeDescriptor((1, "0.0.96.1.1.255", 2))):
|
|
667
|
+
if isinstance(res_read := await ReadByDescriptor(desc).exchange(c), result.Error):
|
|
668
|
+
err.append_err(res_read.err)
|
|
669
|
+
in_e, out_e = res_read.err.split(exc.ResultError)
|
|
670
|
+
if (
|
|
671
|
+
out_e is None
|
|
672
|
+
and in_e.exceptions[0].args[0] == pdu.DataAccessResult(4)
|
|
673
|
+
):
|
|
674
|
+
continue
|
|
675
|
+
else:
|
|
676
|
+
return res_read
|
|
677
|
+
else:
|
|
678
|
+
res = result.Simple(collection.ParameterValue(
|
|
679
|
+
par=desc.instance_id.contents + desc.attribute_id.contents,
|
|
680
|
+
value=res_read.value
|
|
681
|
+
))
|
|
682
|
+
res.propagate_err(err)
|
|
683
|
+
return res
|
|
684
|
+
return err.as_error()
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
@dataclass(frozen=True)
|
|
688
|
+
class KeepAlive(SimpleCopy, OK):
|
|
689
|
+
msg: str = "keep alive(read LND.ln)"
|
|
690
|
+
|
|
691
|
+
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
692
|
+
if isinstance(res := await Par2Data(dlms_par.LDN.value).exchange(c), result.Error):
|
|
693
|
+
return res
|
|
694
|
+
return result.OK
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
class GetLDN(SimpleCopy, CDT[octet_string.LDN]):
|
|
698
|
+
""":return LDN value"""
|
|
699
|
+
msg: str = "get LDN"
|
|
700
|
+
|
|
701
|
+
async def exchange(self, c: Client) -> result.SimpleOrError[octet_string.LDN]:
|
|
702
|
+
if isinstance(res := await ReadByDescriptor(collection.AttrDesc.LDN_VALUE).exchange(c), result.Error):
|
|
703
|
+
return res
|
|
704
|
+
return result.Simple(octet_string.LDN(res.value))
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
# todo: possible implementation ConditionalTask
|
|
708
|
+
# def compare_ldn(man: bytes, ldn: octet_string.LDN) -> bool:
|
|
709
|
+
# return man == ldn.get_manufacturer()
|
|
710
|
+
#
|
|
711
|
+
#
|
|
712
|
+
# check_LDN = ConditionalTask(
|
|
713
|
+
# precondition_task=GetLDN,
|
|
714
|
+
# comp_value=b"KPZ",
|
|
715
|
+
# predicate=compare_ldn,
|
|
716
|
+
# main_task=None
|
|
717
|
+
# )
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
@dataclass
|
|
721
|
+
class MatchLDN(OK):
|
|
722
|
+
universal: bool = field(default=False)
|
|
723
|
+
msg: str = "matching LDN"
|
|
724
|
+
|
|
725
|
+
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
726
|
+
if isinstance((res := await GetLDN().exchange(c)), result.Error):
|
|
727
|
+
return res.with_msg("match LDN")
|
|
728
|
+
if c.objects.LDN.value is None:
|
|
729
|
+
c._objects.LDN.set_attr(2, res.value)
|
|
730
|
+
elif c._objects.LDN.value == res.value:
|
|
731
|
+
"""secret matching"""
|
|
732
|
+
elif self.universal:
|
|
733
|
+
c.log(logL.WARN, F"connected to other server, change LDN")
|
|
734
|
+
await init_type.exchange(c) # todo: maybe set spec?
|
|
735
|
+
else:
|
|
736
|
+
return result.Error.from_e(ValueError(F"got LDN: {res.value}, expected {c._objects.LDN.value}"))
|
|
737
|
+
return result.OK
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
match_ldn = MatchLDN()
|
|
741
|
+
change_ldn = MatchLDN(universal=True)
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
@dataclass
|
|
745
|
+
class Lock:
|
|
746
|
+
__lock: asyncio.Lock = field(init=False, default_factory=asyncio.Lock)
|
|
747
|
+
piece: float = field(default=0.8)
|
|
748
|
+
name: str = ""
|
|
749
|
+
|
|
750
|
+
async def acquire(self, c: Client):
|
|
751
|
+
keep_alive = KeepAlive(self.name)
|
|
752
|
+
while True:
|
|
753
|
+
try:
|
|
754
|
+
await asyncio.wait_for(self.__lock.acquire(), c.com_profile.parameters.inactivity_time_out * self.piece) # todo: make not custom <inactivity_time_out>
|
|
755
|
+
return
|
|
756
|
+
except TimeoutError as e:
|
|
757
|
+
await keep_alive.exchange(c)
|
|
758
|
+
|
|
759
|
+
def release(self):
|
|
760
|
+
self.__lock.release()
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
@dataclass
|
|
764
|
+
class CreateType(SimpleCopy, Simple[collection.Collection]):
|
|
765
|
+
col_id: collection.ID
|
|
766
|
+
msg: str = "CreateType".__class__.__name__
|
|
767
|
+
obj_list: Optional[cdt.Array] = None
|
|
768
|
+
wait_list: asyncio.Lock = field(init=False)
|
|
769
|
+
"""wait <object list>"""
|
|
770
|
+
queue: asyncio.Queue = field(default_factory=asyncio.Queue)
|
|
771
|
+
col: collection.Collection = field(init=False)
|
|
772
|
+
|
|
773
|
+
def __post_init__(self):
|
|
774
|
+
self.col = collection.Collection(id_=self.col_id)
|
|
775
|
+
"""common collection"""
|
|
776
|
+
self.wait_list = Lock(name="wait <object list>")
|
|
777
|
+
|
|
778
|
+
async def exchange(self, c: Client) -> result.Simple[collection.Collection]:
|
|
779
|
+
res = result.Simple(self.col)
|
|
780
|
+
await self.wait_list.acquire(c)
|
|
781
|
+
try:
|
|
782
|
+
if self.obj_list is None:
|
|
783
|
+
if isinstance(res_obj_list := await ReadByDescriptor(collection.AttrDesc.OBJECT_LIST).exchange(c), result.Error):
|
|
784
|
+
return res_obj_list
|
|
785
|
+
self.obj_list = cdt.Array(res_obj_list.value, type_=structs.ObjectListElement)
|
|
786
|
+
if len(self.col) == 0:
|
|
787
|
+
for country_olel in filter(lambda it: ln_pattern.COUNTRY_SPECIFIC == it.logical_name, self.obj_list):
|
|
788
|
+
self.col.set_country(collection.CountrySpecificIdentifiers(country_olel.logical_name.d))
|
|
789
|
+
c.log(logL.INFO, F"set country: {self.col.country}")
|
|
790
|
+
match self.col.country:
|
|
791
|
+
case collection.CountrySpecificIdentifiers.RUSSIA:
|
|
792
|
+
country_desc = collection.AttrDesc.SPODES_VERSION
|
|
793
|
+
case _:
|
|
794
|
+
country_desc = None
|
|
795
|
+
if (
|
|
796
|
+
country_desc is not None
|
|
797
|
+
and next(filter(lambda it: country_desc.instance_id.contents == it.logical_name.contents, self.obj_list), False)
|
|
798
|
+
):
|
|
799
|
+
if isinstance(res_country_ver := await ReadByDescriptor(country_desc).exchange(c), result.Error):
|
|
800
|
+
return res_country_ver
|
|
801
|
+
self.col.set_country_ver(collection.ParameterValue(
|
|
802
|
+
par=country_desc.instance_id.contents + country_desc.attribute_id.contents,
|
|
803
|
+
value=res_country_ver.value
|
|
804
|
+
))
|
|
805
|
+
c.log(logL.INFO, F"set country version: {self.col.country_ver}")
|
|
806
|
+
else:
|
|
807
|
+
c.log(logL.WARN, "was not find <country specific code version> in object_list")
|
|
808
|
+
break
|
|
809
|
+
else:
|
|
810
|
+
c.log(logL.WARN, "was not find <country specific code> in object_list")
|
|
811
|
+
self.col.spec_map = self.col.get_spec()
|
|
812
|
+
for o_l_el in self.obj_list:
|
|
813
|
+
o_l_el: structs.ObjectListElement
|
|
814
|
+
try:
|
|
815
|
+
self.col.add_if_missing( # todo: remove add_if?
|
|
816
|
+
class_id=ut.CosemClassId(int(o_l_el.class_id)),
|
|
817
|
+
version=o_l_el.version,
|
|
818
|
+
logical_name=o_l_el.logical_name)
|
|
819
|
+
except collection.CollectionMapError as e:
|
|
820
|
+
res.append_err(e)
|
|
821
|
+
self.col.add_if_missing(
|
|
822
|
+
class_id=overview.ClassID.DATA,
|
|
823
|
+
version=None, # todo: check version else set 0
|
|
824
|
+
logical_name=cst.LogicalName.from_obis("0.0.42.0.0.255")) # todo: make better
|
|
825
|
+
for ass in self.col.iter_classID_objects(overview.ClassID.ASSOCIATION_LN):
|
|
826
|
+
ass: collection.AssociationLN
|
|
827
|
+
if ass.logical_name.e != 0:
|
|
828
|
+
await ReadObjAttr(ass, 3).exchange(c) # todo: remove from self.queue
|
|
829
|
+
if ass.associated_partners_id.client_SAP == c.SAP:
|
|
830
|
+
cur_ass = ass
|
|
831
|
+
break
|
|
832
|
+
else: # use current association if no variant
|
|
833
|
+
cur_ass = self.col.add_if_missing(
|
|
834
|
+
class_id=overview.ClassID.ASSOCIATION_LN,
|
|
835
|
+
version=None,
|
|
836
|
+
logical_name=cst.LogicalName.from_obis("0.0.40.0.0.255")
|
|
837
|
+
)
|
|
838
|
+
await ReadObjAttr(cur_ass, 3).exchange(c)
|
|
839
|
+
cur_ass.set_attr(2, self.obj_list)
|
|
840
|
+
if cur_ass.associated_partners_id.client_SAP != c.SAP:
|
|
841
|
+
c.log(logL.ERR, F"Wrong current server SAP: {c.SAP} use {cur_ass.associated_partners_id.client_SAP}")
|
|
842
|
+
self.queue.put_nowait((cur_ass, 3)) # read forcibly <associated_partners_id>: use in MapTypeCreator(by <has_sap> method)
|
|
843
|
+
reduce_ln = ln_pattern.LNPattern.parse("0.0.(40,42).0.0.255")
|
|
844
|
+
"""reduced objects for read"""
|
|
845
|
+
for o_l_el in cur_ass.object_list: # todo: read necessary data for create_type
|
|
846
|
+
if reduce_ln == o_l_el.logical_name:
|
|
847
|
+
"""nothing do it"""
|
|
848
|
+
else:
|
|
849
|
+
if (obj := self.col.get(o_l_el.logical_name.contents)) is None:
|
|
850
|
+
continue
|
|
851
|
+
for access in o_l_el.access_rights.attribute_access:
|
|
852
|
+
i = int(access.attribute_id)
|
|
853
|
+
if (
|
|
854
|
+
i == 1 # skip LN
|
|
855
|
+
or not access.access_mode.is_readable() # skip not readable
|
|
856
|
+
or ( # skip early gotten object_list
|
|
857
|
+
cur_ass.logical_name == o_l_el.logical_name
|
|
858
|
+
and i == 2
|
|
859
|
+
) or (
|
|
860
|
+
access.access_mode.is_writable() # skip unknown type writable element
|
|
861
|
+
and not ( # except for:
|
|
862
|
+
isinstance(obj.get_attr_element(i).DATA_TYPE, ut.CHOICE)
|
|
863
|
+
or (
|
|
864
|
+
isinstance(obj, collection.ProfileGeneric)
|
|
865
|
+
and i == 3)
|
|
866
|
+
)
|
|
867
|
+
) or obj.get_attr_element(i).classifier == collection.ic.Classifier.DYNAMIC # skip DYNAMIC
|
|
868
|
+
):
|
|
869
|
+
continue
|
|
870
|
+
self.queue.put_nowait((obj, i))
|
|
871
|
+
for d_id in collection.get_filtered(
|
|
872
|
+
objects=self.col,
|
|
873
|
+
keys=(
|
|
874
|
+
ln_pattern.DEVICE_ID,
|
|
875
|
+
ln_pattern.PROGRAM_ENTRIES)):
|
|
876
|
+
self.queue.put_nowait((d_id, 2)) # todo: make second queue2 for ReadEmptyAttribute(d_id.logical_name, 2).exchange(c)
|
|
877
|
+
except TimeoutError as e:
|
|
878
|
+
c.log(logL.ERR, F"can't got <object list>: {e}")
|
|
879
|
+
finally:
|
|
880
|
+
self.wait_list.release()
|
|
881
|
+
while True:
|
|
882
|
+
try:
|
|
883
|
+
obj, i = self.queue.get_nowait()
|
|
884
|
+
except asyncio.QueueEmpty as e:
|
|
885
|
+
c.log(logL.INFO, "QueueEmpty")
|
|
886
|
+
try:
|
|
887
|
+
await asyncio.wait_for(self.queue.join(), c.com_profile.parameters.inactivity_time_out) # todo: why whis timeout?? # todo: make not custom <inactivity_time_out>
|
|
888
|
+
break
|
|
889
|
+
except TimeoutError as e:
|
|
890
|
+
c.log(logL.INFO, "wait returned tasks in queue")
|
|
891
|
+
continue # wait returned tasks in queue
|
|
892
|
+
try:
|
|
893
|
+
await c.read_attribute(obj, i)
|
|
894
|
+
except TimeoutError as e:
|
|
895
|
+
c.log(logL.ERR, F"return {obj}:{i} in queue: {e}")
|
|
896
|
+
await self.queue.put((obj, i))
|
|
897
|
+
except exc.Timeout as e:
|
|
898
|
+
c.log(logL.ERR, F"break create type {self.col} by {e}")
|
|
899
|
+
await self.queue.put((obj, i))
|
|
900
|
+
raise e
|
|
901
|
+
except AttributeError as e:
|
|
902
|
+
c.log(logL.ERR, F"skip value wrong value for {obj}:{i}: {e}")
|
|
903
|
+
except Exception as e: # todo: make better!!!
|
|
904
|
+
c.log(logL.ERR, F"skip value wrong value for {obj}:{i}: {e}")
|
|
905
|
+
finally:
|
|
906
|
+
self.queue.task_done()
|
|
907
|
+
print("stop create")
|
|
908
|
+
return res
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
type ID_SAP = tuple[collection.ID, enums.ClientSAP]
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
@dataclass(frozen=True)
|
|
915
|
+
class IDSAP:
|
|
916
|
+
id: collection.ID
|
|
917
|
+
sap: enums.ClientSAP
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
@dataclass
|
|
921
|
+
class NonInit:
|
|
922
|
+
msg: str
|
|
923
|
+
|
|
924
|
+
def __getattr__(self, item):
|
|
925
|
+
raise RuntimeError(self.msg)
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
class MapTypeCreator:
|
|
929
|
+
adapter: Adapter
|
|
930
|
+
lock: asyncio.Lock
|
|
931
|
+
con: dict[IDSAP, CreateType]
|
|
932
|
+
|
|
933
|
+
def __init__(self, adapter: Adapter):
|
|
934
|
+
self.adapter = adapter
|
|
935
|
+
self.con = dict()
|
|
936
|
+
self.lock = asyncio.Lock()
|
|
937
|
+
|
|
938
|
+
async def get_collection(
|
|
939
|
+
self,
|
|
940
|
+
c: Client,
|
|
941
|
+
col_id: collection.ID
|
|
942
|
+
) -> result.Simple[collection.Collection]:
|
|
943
|
+
new_col: collection.Collection
|
|
944
|
+
err: Errors
|
|
945
|
+
id_sap = IDSAP(col_id, c.SAP)
|
|
946
|
+
async with self.lock:
|
|
947
|
+
if id_sap in self.con.keys():
|
|
948
|
+
c.log(logL.INFO, F"{self.__class__.__name__} {col_id} already in container")
|
|
949
|
+
else:
|
|
950
|
+
c.log(logL.INFO, F"{self.__class__.__name__} register new collection: {col_id}")
|
|
951
|
+
self.con[id_sap] = CreateType(col_id)
|
|
952
|
+
res = await self.con[id_sap].exchange(c)
|
|
953
|
+
async with self.lock:
|
|
954
|
+
try:
|
|
955
|
+
gotten, _ = self.adapter.get_collection(col_id).unpack() # check for first update
|
|
956
|
+
if gotten.has_sap(id_sap.sap):
|
|
957
|
+
"""not need keep"""
|
|
958
|
+
else:
|
|
959
|
+
self.adapter.set_collection(res.value) # todo: make as ADAPTER.merge_collection(ret)
|
|
960
|
+
except AdapterException as e:
|
|
961
|
+
self.adapter.set_collection(res.value)
|
|
962
|
+
res.value, err = res.value.copy().unpack()
|
|
963
|
+
if err is not None:
|
|
964
|
+
res.append_err(err)
|
|
965
|
+
return res
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
@dataclass
|
|
969
|
+
class InitType(SimpleCopy, Simple[collection.Collection]):
|
|
970
|
+
adapter: Adapter
|
|
971
|
+
msg: str = "initiate type"
|
|
972
|
+
|
|
973
|
+
async def exchange(self, c: Client) -> result.SimpleOrError[collection.Collection]:
|
|
974
|
+
if isinstance((res := await Sequence(
|
|
975
|
+
GetLDN(),
|
|
976
|
+
FindFirmwareId(),
|
|
977
|
+
FindFirmwareVersion(),
|
|
978
|
+
msg="get collection.ID",
|
|
979
|
+
).exchange(c)), result.Error):
|
|
980
|
+
return res.with_msg("init type")
|
|
981
|
+
ldn, f_id, f_ver = res.value
|
|
982
|
+
col_id = collection.ID(ldn.get_manufacturer(), f_id, f_ver)
|
|
983
|
+
try:
|
|
984
|
+
if (res := self.adapter.get_collection(col_id)).value.has_sap(c.SAP):
|
|
985
|
+
c.log(logL.INFO, F"find collection in {self.adapter}")
|
|
986
|
+
else:
|
|
987
|
+
raise AdapterException(F"was found collection from adapter with absent current {c.SAP}")
|
|
988
|
+
except AdapterException as e:
|
|
989
|
+
c.log(logL.WARN, F"not find into adapter: {e}")
|
|
990
|
+
res = await map_type_creator.get_collection(
|
|
991
|
+
c=c,
|
|
992
|
+
col_id=col_id
|
|
993
|
+
)
|
|
994
|
+
c._objects = res.value
|
|
995
|
+
c._objects.LDN.set_attr(2, ldn)
|
|
996
|
+
return res
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
init_type: InitType
|
|
1000
|
+
"""init after get_adapter"""
|
|
1001
|
+
map_type_creator: MapTypeCreator
|
|
1002
|
+
"""init after get_adapter"""
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
def get_adapter(value: Adapter):
|
|
1006
|
+
global map_type_creator, init_type
|
|
1007
|
+
map_type_creator = MapTypeCreator(value)
|
|
1008
|
+
init_type = InitType(value)
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
get_adapter(gag) # Dummy Adapter
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
@dataclass
|
|
1015
|
+
@deprecated("use <ReadObjAttr>")
|
|
1016
|
+
class ReadAttribute(SimpleCopy, CDT):
|
|
1017
|
+
ln: collection.LNContaining
|
|
1018
|
+
index: int
|
|
1019
|
+
msg: str = "read LN attribute"
|
|
1020
|
+
|
|
1021
|
+
async def exchange(self, c: Client) -> result.Simple[cdt.CommonDataType]:
|
|
1022
|
+
obj = c.objects.get_object(self.ln)
|
|
1023
|
+
return await ReadObjAttr(obj, self.index).exchange(c)
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
@dataclass
|
|
1027
|
+
class ReadObjAttr(SimpleCopy, CDT):
|
|
1028
|
+
obj: collection.InterfaceClass
|
|
1029
|
+
index: int
|
|
1030
|
+
msg: str = "read object attribute"
|
|
1031
|
+
|
|
1032
|
+
async def exchange(self, c: Client) -> result.SimpleOrError[cdt.CommonDataType]:
|
|
1033
|
+
# TODO: check is_readable?
|
|
1034
|
+
c.get_get_request_normal(
|
|
1035
|
+
attr_desc=self.obj.get_attr_descriptor(
|
|
1036
|
+
value=self.index,
|
|
1037
|
+
with_selection=bool(c.negotiated_conformance.selective_access)))
|
|
1038
|
+
start_read_time: float = time.perf_counter()
|
|
1039
|
+
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
1040
|
+
return res_pdu
|
|
1041
|
+
c.last_transfer_time = datetime.timedelta(seconds=time.perf_counter()-start_read_time)
|
|
1042
|
+
try:
|
|
1043
|
+
self.obj.set_attr(self.index, res_pdu.value)
|
|
1044
|
+
return result.Simple(self.obj.get_attr(self.index))
|
|
1045
|
+
except ValueError as e:
|
|
1046
|
+
return result.Error.from_e(e)
|
|
1047
|
+
except ut.UserfulTypesException as e:
|
|
1048
|
+
return result.Error.from_e(e)
|
|
1049
|
+
except exc.DLMSException as e:
|
|
1050
|
+
return result.Error.from_e(e)
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
@dataclass(frozen=True)
|
|
1054
|
+
class Par2Data[T: cdt.CommonDataType](SimpleCopy, CDT[T]):
|
|
1055
|
+
"""get CommonDataType by Parameter"""
|
|
1056
|
+
par: Parameter
|
|
1057
|
+
msg: str = "read data by Parameter"
|
|
1058
|
+
|
|
1059
|
+
async def exchange(self, c: Client) -> result.SimpleOrError[T]:
|
|
1060
|
+
if isinstance((res_obj := c.objects.par2obj(self.par)), result.Error):
|
|
1061
|
+
return res_obj
|
|
1062
|
+
if isinstance((res := await ReadObjAttr(res_obj.value, self.par.i).exchange(c)), result.Error):
|
|
1063
|
+
return res
|
|
1064
|
+
for el in self.par.elements():
|
|
1065
|
+
res.value = res.value[el]
|
|
1066
|
+
return res
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
class ReadSequence(List):
|
|
1070
|
+
tasks: list[ReadAttribute]
|
|
1071
|
+
|
|
1072
|
+
def __post_init__(self):
|
|
1073
|
+
assert all((isinstance(t, ReadAttribute) for t in self.tasks))
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
type AttrValueComp = Callable[[cdt.CommonDataType | None], bool]
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
def is_empty(value: cdt.CommonDataType | None) -> bool:
|
|
1080
|
+
"""is empty attribute value"""
|
|
1081
|
+
return True if value is None else False
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
@dataclass(frozen=True)
|
|
1085
|
+
class ReadAttributeIf(SimpleCopy, Base):
|
|
1086
|
+
"""read if func with arg as value is True"""
|
|
1087
|
+
ln: collection.LNContaining
|
|
1088
|
+
index: int
|
|
1089
|
+
func: AttrValueComp
|
|
1090
|
+
msg: str = "read attribute with condition"
|
|
1091
|
+
|
|
1092
|
+
async def exchange(self, c: Client) -> result.Simple[cdt.CommonDataType]:
|
|
1093
|
+
# TODO: check is_readable
|
|
1094
|
+
obj = c.objects.get_object(self.ln)
|
|
1095
|
+
if self.func(obj.get_attr(self.index)):
|
|
1096
|
+
return await ReadAttribute(
|
|
1097
|
+
ln=self.ln,
|
|
1098
|
+
index=self.index).exchange(c)
|
|
1099
|
+
else:
|
|
1100
|
+
return result.OK
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
@dataclass(frozen=True)
|
|
1104
|
+
class ReadEmptyAttribute(SimpleCopy, Base):
|
|
1105
|
+
ln: collection.LNContaining
|
|
1106
|
+
index: int
|
|
1107
|
+
msg: str = "read if attribute is empty"
|
|
1108
|
+
|
|
1109
|
+
async def exchange(self, c: Client) -> result.Simple[cdt.CommonDataType]:
|
|
1110
|
+
# TODO: check is_readable
|
|
1111
|
+
return await ReadAttributeIf(
|
|
1112
|
+
ln=self.ln,
|
|
1113
|
+
index=self.index,
|
|
1114
|
+
func=is_empty
|
|
1115
|
+
).exchange(c)
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
@dataclass(frozen=True)
|
|
1119
|
+
class ReadWritableAttributes(SimpleCopy, Base):
|
|
1120
|
+
ln: collection.LNContaining
|
|
1121
|
+
indexes: tuple[int, ...]
|
|
1122
|
+
msg: str = "read only writable attribute"
|
|
1123
|
+
|
|
1124
|
+
async def exchange(self, c: Client) -> result.List[cdt.CommonDataType]:
|
|
1125
|
+
# TODO: check is_readable
|
|
1126
|
+
res = result.List()
|
|
1127
|
+
indexes: list[int] = []
|
|
1128
|
+
ass: collection.AssociationLN = c._objects.sap2association(c.SAP)
|
|
1129
|
+
for i in self.indexes:
|
|
1130
|
+
if ass.is_writable(self.ln, i):
|
|
1131
|
+
indexes.append(i)
|
|
1132
|
+
if len(indexes) != 0:
|
|
1133
|
+
res.append(await ReadAttributes(
|
|
1134
|
+
ln=self.ln,
|
|
1135
|
+
indexes=tuple(indexes)
|
|
1136
|
+
).exchange(c))
|
|
1137
|
+
else:
|
|
1138
|
+
res.append(result.OK)
|
|
1139
|
+
c.log(logL.INFO, F"skip {self.__class__.__name__} operation, all is actually")
|
|
1140
|
+
return res
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
# copy past from ReadWritableAttributes
|
|
1144
|
+
@dataclass(frozen=True)
|
|
1145
|
+
class ActualizeAttributes(SimpleCopy, OK):
|
|
1146
|
+
ln: collection.LNContaining
|
|
1147
|
+
indexes: tuple[int, ...]
|
|
1148
|
+
msg: str = "read if attribute is empty or writable"
|
|
1149
|
+
|
|
1150
|
+
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1151
|
+
# TODO: check is_readable
|
|
1152
|
+
ass: collection.AssociationLN = c.objects.sap2association(c.SAP)
|
|
1153
|
+
obj = c.objects.get_object(self.ln)
|
|
1154
|
+
indexes = [
|
|
1155
|
+
i for i in self.indexes if (
|
|
1156
|
+
obj.get_attr(i) is None
|
|
1157
|
+
or obj.get_attr_element(i).classifier == ic.Classifier.DYNAMIC
|
|
1158
|
+
or ass.is_writable(self.ln, i)
|
|
1159
|
+
)
|
|
1160
|
+
]
|
|
1161
|
+
if len(indexes) != 0:
|
|
1162
|
+
if isinstance((res := await ReadAttributes(
|
|
1163
|
+
ln=self.ln,
|
|
1164
|
+
indexes=tuple(indexes)
|
|
1165
|
+
).exchange(c)), result.Error):
|
|
1166
|
+
return res
|
|
1167
|
+
return result.OK
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
@dataclass(frozen=True)
|
|
1171
|
+
class ReadAttributes(SimpleCopy, _List[cdt.CommonDataType]):
|
|
1172
|
+
ln: collection.LNContaining
|
|
1173
|
+
indexes: tuple[int, ...]
|
|
1174
|
+
msg: str = ""
|
|
1175
|
+
|
|
1176
|
+
async def exchange(self, c: Client) -> result.List[cdt.CommonDataType] | result.Error:
|
|
1177
|
+
res = result.List()
|
|
1178
|
+
obj = c.objects.get_object(self.ln)
|
|
1179
|
+
# TODO: check for Get-Request-With-List
|
|
1180
|
+
for i in self.indexes:
|
|
1181
|
+
res.append(await ReadObjAttr(obj, i).exchange(c))
|
|
1182
|
+
return res
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
@dataclass
|
|
1186
|
+
@deprecated("use <Write2>")
|
|
1187
|
+
class Write(SimpleCopy, OK):
|
|
1188
|
+
"""write with ParameterData struct"""
|
|
1189
|
+
par_data: ParData
|
|
1190
|
+
msg: str = "write attribute"
|
|
1191
|
+
|
|
1192
|
+
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1193
|
+
if isinstance(res_obj := c.objects.par2obj(self.par_data.par), result.Error):
|
|
1194
|
+
return res_obj
|
|
1195
|
+
if self.par_data.par.n_elements == 0:
|
|
1196
|
+
enc = self.par_data.data.encoding
|
|
1197
|
+
elif isinstance(res_read := await ReadObjAttr(res_obj.value, self.par_data.par.i).exchange(c), result.Error):
|
|
1198
|
+
return res_read
|
|
1199
|
+
else:
|
|
1200
|
+
data = a_data
|
|
1201
|
+
for el in self.par_data.par.elements():
|
|
1202
|
+
data = data[el]
|
|
1203
|
+
data.set(self.par_data.data)
|
|
1204
|
+
enc = data.encoding
|
|
1205
|
+
data = c.get_set_request_normal(
|
|
1206
|
+
obj=res_obj.value,
|
|
1207
|
+
attr_index=self.par_data.par.i,
|
|
1208
|
+
value=enc)
|
|
1209
|
+
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
1210
|
+
return res_pdu
|
|
1211
|
+
return result.OK
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
@dataclass(frozen=True)
|
|
1215
|
+
class Write2(SimpleCopy, OK):
|
|
1216
|
+
"""write with ParameterData struct"""
|
|
1217
|
+
par: Parameter
|
|
1218
|
+
data: cdt.CommonDataType
|
|
1219
|
+
msg: str = "write Data"
|
|
1220
|
+
|
|
1221
|
+
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1222
|
+
if isinstance(res_obj := c.objects.par2obj(self.par), result.Error):
|
|
1223
|
+
return res_obj
|
|
1224
|
+
if self.par.n_elements == 0:
|
|
1225
|
+
enc = self.data.encoding
|
|
1226
|
+
elif isinstance(res_read := await Par2Data[cdt.CommonDataType](self.par.attr).exchange(c), result.Error):
|
|
1227
|
+
return res_read
|
|
1228
|
+
else:
|
|
1229
|
+
data = res_read.value
|
|
1230
|
+
for el in self.par.elements():
|
|
1231
|
+
data = data[el]
|
|
1232
|
+
data.set(self.data)
|
|
1233
|
+
enc = data.encoding
|
|
1234
|
+
data = c.get_set_request_normal(
|
|
1235
|
+
obj=res_obj.value,
|
|
1236
|
+
attr_index=self.par.i,
|
|
1237
|
+
value=enc)
|
|
1238
|
+
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
1239
|
+
return res_pdu
|
|
1240
|
+
return result.OK
|
|
1241
|
+
|
|
1242
|
+
|
|
1243
|
+
@dataclass(frozen=True)
|
|
1244
|
+
class SetBits(SimpleCopy, OK):
|
|
1245
|
+
"""set bits by pattern"""
|
|
1246
|
+
par: Parameter
|
|
1247
|
+
pattern: dict[int, int]
|
|
1248
|
+
msg: str = "set bits"
|
|
1249
|
+
|
|
1250
|
+
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1251
|
+
if isinstance(res_obj := c.objects.par2obj(self.par), result.Error):
|
|
1252
|
+
return res_obj
|
|
1253
|
+
if isinstance(res_read := await Par2Data[cdt.Digital | cdt.BitString](self.par.attr).exchange(c), result.Error):
|
|
1254
|
+
return res_read
|
|
1255
|
+
data = res_read.value
|
|
1256
|
+
for el in self.par.elements():
|
|
1257
|
+
data = data[el]
|
|
1258
|
+
if isinstance(data, cdt.Digital):
|
|
1259
|
+
data_int = int(data)
|
|
1260
|
+
for pos, val in self.pattern.items():
|
|
1261
|
+
data_int = (data_int | (1 << pos)) if val else (data_int & ~(1 << pos))
|
|
1262
|
+
data.set(data_int)
|
|
1263
|
+
elif isinstance(data, cdt.BitString):
|
|
1264
|
+
for pos, val in self.pattern.items():
|
|
1265
|
+
try:
|
|
1266
|
+
data[pos] = val
|
|
1267
|
+
except IndexError as e:
|
|
1268
|
+
return result.Error.from_e(ValueError(f"can't set bit {pos} in {self.par}"))
|
|
1269
|
+
else:
|
|
1270
|
+
return result.Error.from_e(TypeError(f"{self.par} not available SetBits"))
|
|
1271
|
+
c.get_set_request_normal(
|
|
1272
|
+
obj=res_obj.value,
|
|
1273
|
+
attr_index=self.par.i,
|
|
1274
|
+
value=res_read.value.encoding)
|
|
1275
|
+
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
1276
|
+
return res_pdu
|
|
1277
|
+
return result.OK
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
@dataclass
|
|
1281
|
+
@deprecated("use <WriteTranscript>")
|
|
1282
|
+
class WriteParValue(SimpleCopy, OK):
|
|
1283
|
+
"""write with ParameterValues struct"""
|
|
1284
|
+
par_value: ParValues[cdt.Transcript]
|
|
1285
|
+
msg: str = "write attribute by Transcript"
|
|
1286
|
+
|
|
1287
|
+
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1288
|
+
if isinstance(res1 := c.objects.par2obj(self.par_value.par), result.Error):
|
|
1289
|
+
return res1
|
|
1290
|
+
obj = res1.value
|
|
1291
|
+
if (a_data := res1.value.get_attr(self.par_value.par.i)) is None:
|
|
1292
|
+
if isinstance((res := await Par2Data(self.par_value.par).exchange(c)), result.Error):
|
|
1293
|
+
return res
|
|
1294
|
+
else:
|
|
1295
|
+
a_data = res.value
|
|
1296
|
+
s_u = c._objects.par2su(self.par_value.par)
|
|
1297
|
+
if isinstance(s_u, cdt.ScalUnitType):
|
|
1298
|
+
value: cdt.Transcript = str(float(self.par_value.data) * 10 ** -int(s_u.scaler))
|
|
1299
|
+
else:
|
|
1300
|
+
value = self.par_value.data
|
|
1301
|
+
if self.par_value.par.n_elements == 0:
|
|
1302
|
+
set_data = a_data.parse(value)
|
|
1303
|
+
elif isinstance((res := await Par2Data(par).exchange(c)), result.Error):
|
|
1304
|
+
return res
|
|
1305
|
+
else:
|
|
1306
|
+
data = a_data
|
|
1307
|
+
for el in self.par_value.par.elements():
|
|
1308
|
+
data = data[el]
|
|
1309
|
+
new_data = data.parse(value) # todo: can't use with CHOICE
|
|
1310
|
+
data.set(new_data)
|
|
1311
|
+
set_data = a_data
|
|
1312
|
+
data = c.get_set_request_normal(
|
|
1313
|
+
obj=obj,
|
|
1314
|
+
attr_index=self.par_value.par.i,
|
|
1315
|
+
value=set_data.encoding)
|
|
1316
|
+
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
1317
|
+
return res_pdu
|
|
1318
|
+
return result.OK
|
|
1319
|
+
|
|
1320
|
+
|
|
1321
|
+
@dataclass
|
|
1322
|
+
@deprecated("use <Write>")
|
|
1323
|
+
class WriteAttribute(SimpleCopy, OK):
|
|
1324
|
+
ln: collection.LNContaining
|
|
1325
|
+
index: int
|
|
1326
|
+
value: bytes | str | int | list | tuple | datetime.datetime
|
|
1327
|
+
msg: str = ""
|
|
1328
|
+
|
|
1329
|
+
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1330
|
+
obj = c._objects.get_object(self.ln)
|
|
1331
|
+
if isinstance(self.value, (str, int, list, tuple, datetime.datetime)):
|
|
1332
|
+
value2 = await c.encode(
|
|
1333
|
+
obj=obj,
|
|
1334
|
+
index=self.index,
|
|
1335
|
+
value=self.value)
|
|
1336
|
+
enc = value2.encoding
|
|
1337
|
+
else:
|
|
1338
|
+
enc = self.value
|
|
1339
|
+
data = c.get_set_request_normal(
|
|
1340
|
+
obj=obj,
|
|
1341
|
+
attr_index=self.index,
|
|
1342
|
+
value=enc)
|
|
1343
|
+
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
1344
|
+
return res_pdu
|
|
1345
|
+
return result.OK # todo: return Data-Access-Result
|
|
1346
|
+
|
|
1347
|
+
|
|
1348
|
+
@deprecated("use Execute")
|
|
1349
|
+
@dataclass
|
|
1350
|
+
class ExecuteByDesc(SimpleCopy, Base):
|
|
1351
|
+
"""execute method by method descriptor # TODO: rewrite this"""
|
|
1352
|
+
desc: ut.CosemMethodDescriptor
|
|
1353
|
+
msg: str = "old execute"
|
|
1354
|
+
|
|
1355
|
+
async def exchange(self, c: Client) -> result.Result:
|
|
1356
|
+
try:
|
|
1357
|
+
await c.execute_method(self.desc)
|
|
1358
|
+
return result.Simple(pdu.ActionResult.SUCCESS)
|
|
1359
|
+
except Exception as e:
|
|
1360
|
+
return result.Error.from_e(exc.DLMSException(F'Исполнение {self.desc}'))
|
|
1361
|
+
|
|
1362
|
+
|
|
1363
|
+
@deprecated("use Execute2")
|
|
1364
|
+
@dataclass
|
|
1365
|
+
class Execute(SimpleCopy, OK):
|
|
1366
|
+
"""execute method"""
|
|
1367
|
+
ln: collection.LNContaining
|
|
1368
|
+
index: int
|
|
1369
|
+
value: cdt.CommonDataType = None # todo: maybe use simple bytes
|
|
1370
|
+
msg: str = "execute method"
|
|
1371
|
+
|
|
1372
|
+
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1373
|
+
obj = c._objects.get_object(self.ln)
|
|
1374
|
+
try:
|
|
1375
|
+
await c.execute_method2(obj, self.index, self.value)
|
|
1376
|
+
return result.OK
|
|
1377
|
+
except Exception as e:
|
|
1378
|
+
return result.Error.from_e(exc.DLMSException(F'Исполнение {self.desc}'))
|
|
1379
|
+
|
|
1380
|
+
|
|
1381
|
+
@dataclass(frozen=True)
|
|
1382
|
+
class Execute2(SimpleCopy, OK):
|
|
1383
|
+
"""execute method"""
|
|
1384
|
+
par: Parameter
|
|
1385
|
+
data: cdt.CommonDataType
|
|
1386
|
+
msg: str = "Execute method"
|
|
1387
|
+
|
|
1388
|
+
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1389
|
+
if isinstance(res := c.objects.par2obj(self.par), result.Error):
|
|
1390
|
+
return res
|
|
1391
|
+
try:
|
|
1392
|
+
data = c.get_action_request_normal(
|
|
1393
|
+
meth_desc=res.value.get_meth_descriptor(self.par.i),
|
|
1394
|
+
method=self.data
|
|
1395
|
+
)
|
|
1396
|
+
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
1397
|
+
return res_pdu
|
|
1398
|
+
return result.OK
|
|
1399
|
+
except Exception as e:
|
|
1400
|
+
return result.Error.from_e(e)
|
|
1401
|
+
|
|
1402
|
+
|
|
1403
|
+
@dataclass
|
|
1404
|
+
class GetTimeDelta(SimpleCopy, Simple[float]):
|
|
1405
|
+
"""Read and return <time delta> in second: """
|
|
1406
|
+
msg: str = "Read Clock.time"
|
|
1407
|
+
|
|
1408
|
+
async def exchange(self, c: Client) -> result.SimpleOrError[float]:
|
|
1409
|
+
acc = result.ErrorAccumulator()
|
|
1410
|
+
obj = c._objects.clock
|
|
1411
|
+
if isinstance(
|
|
1412
|
+
res_read_tz := await ReadObjAttr(
|
|
1413
|
+
obj=obj,
|
|
1414
|
+
index=3
|
|
1415
|
+
).exchange(c),
|
|
1416
|
+
result.Error):
|
|
1417
|
+
return res_read_tz
|
|
1418
|
+
tz = datetime.timezone(datetime.timedelta(minutes=int(res_read_tz.value)))
|
|
1419
|
+
if isinstance(
|
|
1420
|
+
res_read := await ReadObjAttr(
|
|
1421
|
+
obj=obj,
|
|
1422
|
+
index=2
|
|
1423
|
+
).exchange(c),
|
|
1424
|
+
result.Error):
|
|
1425
|
+
return res_read
|
|
1426
|
+
value = datetime.datetime.now(tz=tz)
|
|
1427
|
+
value2 = res_read.value.to_datetime().replace(tzinfo=tz)
|
|
1428
|
+
return result.Simple((value2 - value).total_seconds()).append_err(acc.err)
|
|
1429
|
+
|
|
1430
|
+
|
|
1431
|
+
|
|
1432
|
+
@dataclass
|
|
1433
|
+
class WriteTime(SimpleCopy, Simple[float]):
|
|
1434
|
+
"""Write and return <record time> in second: """
|
|
1435
|
+
limit: float = 5.0
|
|
1436
|
+
number_of_retries: int = 10
|
|
1437
|
+
msg: str = "write Clock.time"
|
|
1438
|
+
|
|
1439
|
+
async def exchange(self, c: Client) -> result.SimpleOrError[float]:
|
|
1440
|
+
acc = result.ErrorAccumulator()
|
|
1441
|
+
obj = c._objects.clock
|
|
1442
|
+
c.get_get_request_normal(obj.get_attr_descriptor(3))
|
|
1443
|
+
if isinstance(res_pdu := await c.read_data_block(), result.Error):
|
|
1444
|
+
return res_pdu
|
|
1445
|
+
tz = obj.get_attr_element(3).DATA_TYPE(res_pdu.value)
|
|
1446
|
+
for i in range(self.number_of_retries):
|
|
1447
|
+
pre_time = time.time()
|
|
1448
|
+
if isinstance(
|
|
1449
|
+
res_write := await WriteAttribute(
|
|
1450
|
+
ln=obj.logical_name,
|
|
1451
|
+
index=2,
|
|
1452
|
+
value=(datetime.datetime.utcnow() + datetime.timedelta(minutes=int(tz)))).exchange(c),
|
|
1453
|
+
result.Error):
|
|
1454
|
+
return res_write
|
|
1455
|
+
rec_time = time.time() - pre_time
|
|
1456
|
+
if rec_time < self.limit:
|
|
1457
|
+
break
|
|
1458
|
+
acc.append_e(TimeoutError(f"can't write in {i} attemp in time"))
|
|
1459
|
+
else:
|
|
1460
|
+
return result.Error.from_e(TimeoutError(f"can't write time for limit: {self.limit} second"))
|
|
1461
|
+
return result.Simple(rec_time).append_err(acc.err)
|
|
1462
|
+
|
|
1463
|
+
|
|
1464
|
+
@dataclass
|
|
1465
|
+
class ImageTransfer(SimpleCopy, StrictOK):
|
|
1466
|
+
par: dlms_par.ImageTransfer
|
|
1467
|
+
image: bytes
|
|
1468
|
+
waiting_for_activation: float = 10.0
|
|
1469
|
+
n_t_b: int = field(init=False, default=0)
|
|
1470
|
+
"""not transferred block"""
|
|
1471
|
+
n_blocks: int = field(init=False)
|
|
1472
|
+
msg: str = "image transfer"
|
|
1473
|
+
|
|
1474
|
+
def __post_init__(self) -> None:
|
|
1475
|
+
self.ITI = ImageTransferInitiate((
|
|
1476
|
+
bytearray(hashlib.md5(self.image).digest()), # todo: make custom this
|
|
1477
|
+
cdt.DoubleLongUnsigned(len(self.image))
|
|
1478
|
+
))
|
|
1479
|
+
|
|
1480
|
+
async def exchange(self, c: Client) -> result.StrictOk | result.Error:
|
|
1481
|
+
""" update image if blocks is fulls ver 3"""
|
|
1482
|
+
offset: int
|
|
1483
|
+
res_block_size: result.SimpleOrError[cdt.DoubleLongUnsigned]
|
|
1484
|
+
res_status: result.SimpleOrError[i_t_status.ImageTransferStatus]
|
|
1485
|
+
res_activate_info: result.SimpleOrError[ImageToActivateInfo]
|
|
1486
|
+
res_ntb: result.SimpleOrError[cdt.DoubleLongUnsigned]
|
|
1487
|
+
res_tbs: result.SimpleOrError[cdt.BitString]
|
|
1488
|
+
previous_status: Optional[i_t_status.ImageTransferStatus] = None
|
|
1489
|
+
res = result.StrictOk()
|
|
1490
|
+
if isinstance(res_block_size := await Par2Data(self.par.image_block_size).exchange(c), result.Error):
|
|
1491
|
+
return res_block_size
|
|
1492
|
+
block_size = int(res_block_size.value)
|
|
1493
|
+
self.n_blocks, mod = divmod(len(self.image), block_size)
|
|
1494
|
+
if mod != 0:
|
|
1495
|
+
self.n_blocks += 1
|
|
1496
|
+
# TODO: need common counter for exit from infinity loop
|
|
1497
|
+
if isinstance(res_status := await Par2Data(self.par.image_transfer_status).exchange(c), result.Error):
|
|
1498
|
+
return res_status
|
|
1499
|
+
if isinstance(res_activate_info := await Par2Data(self.par.image_to_activate_info).exchange(c), result.Error):
|
|
1500
|
+
return res_activate_info
|
|
1501
|
+
if (
|
|
1502
|
+
res_status.value in (i_t_status.TRANSFER_NOT_INITIATED, i_t_status.VERIFICATION_FAILED, i_t_status.ACTIVATION_FAILED)
|
|
1503
|
+
or len(res_activate_info.value) == 0
|
|
1504
|
+
or res_activate_info.value[0].image_to_activate_identification != self.ITI.image_identifier
|
|
1505
|
+
):
|
|
1506
|
+
if isinstance(res_initiate := await Execute2(self.par.image_transfer_initiate, self.ITI).exchange(c), result.Error):
|
|
1507
|
+
return res_initiate
|
|
1508
|
+
c.log(logL.INFO, "Start initiate Image Transfer")
|
|
1509
|
+
if isinstance(res_status := await Par2Data(self.par.image_transfer_status).exchange(c), result.Error):
|
|
1510
|
+
return res_status
|
|
1511
|
+
elif res_status.value == i_t_status.ACTIVATION_SUCCESSFUL:
|
|
1512
|
+
# image in outside memory and already activated early, but erased by hard programming. Need again go to activation
|
|
1513
|
+
res_status.value = i_t_status.VERIFICATION_SUCCESSFUL
|
|
1514
|
+
else:
|
|
1515
|
+
c.log(logL.INFO, "already INITIATED")
|
|
1516
|
+
if isinstance(res_ntb := await Par2Data(self.par.image_first_not_transferred_block_number).exchange(c), result.Error):
|
|
1517
|
+
return res_ntb
|
|
1518
|
+
self.n_t_b = int(res_ntb.value)
|
|
1519
|
+
if self.n_t_b > (len(self.image) / block_size): # all blocks were send
|
|
1520
|
+
if isinstance(res_verify := await VerifyImage(self.par).exchange(c), result.Error):
|
|
1521
|
+
return res_verify
|
|
1522
|
+
c.log(logL.INFO, "Start Verify Transfer")
|
|
1523
|
+
while True:
|
|
1524
|
+
c.log(logL.STATE, F"{res_status.value=}")
|
|
1525
|
+
match res_status.value:
|
|
1526
|
+
case i_t_status.VERIFICATION_FAILED if res_status.value == previous_status:
|
|
1527
|
+
return result.Error.from_e(exc.DLMSException(), "Verification Error")
|
|
1528
|
+
case i_t_status.TRANSFER_INITIATED if res_status.value == previous_status:
|
|
1529
|
+
res.append_e(exc.DLMSException("Expected Switch to Verification Initiated status, got Initiated"))
|
|
1530
|
+
case i_t_status.TRANSFER_NOT_INITIATED:
|
|
1531
|
+
res.append_e(exc.DLMSException("Got Not initiated status after call Initiation"))
|
|
1532
|
+
case i_t_status.TRANSFER_INITIATED:
|
|
1533
|
+
while self.n_t_b < self.n_blocks:
|
|
1534
|
+
offset = self.n_t_b * block_size
|
|
1535
|
+
if isinstance(res_tr_block := await TransferBlock(
|
|
1536
|
+
par=self.par,
|
|
1537
|
+
number=cdt.DoubleLongUnsigned(self.n_t_b),
|
|
1538
|
+
value=cdt.OctetString(bytearray(self.image[offset: offset + block_size]))
|
|
1539
|
+
).exchange(c), result.Error):
|
|
1540
|
+
return res_tr_block
|
|
1541
|
+
self.n_t_b += 1 # todo: maybe get from SERVER - await get_not_transferred_block.exchange(c)
|
|
1542
|
+
c.log(logL.INFO, "All blocks transferred")
|
|
1543
|
+
if isinstance(res_verify := await VerifyImage(self.par).exchange(c), result.Error):
|
|
1544
|
+
return res_verify
|
|
1545
|
+
case i_t_status.VERIFICATION_INITIATED:
|
|
1546
|
+
c.log(logL.INFO, "read bitstring. It must grow")
|
|
1547
|
+
# TODO: calculate time for waiting or read growing bitstring ?
|
|
1548
|
+
if isinstance(res_tbs := await Par2Data(self.par.image_transferred_blocks_status).exchange(c), result.Error):
|
|
1549
|
+
return res_tbs
|
|
1550
|
+
if len(res_tbs.value) < self.n_t_b:
|
|
1551
|
+
c.log(logL.INFO, F"Got blocks[{len(res_tbs.value)}]") # todo: remove
|
|
1552
|
+
else:
|
|
1553
|
+
c.log(logL.INFO, "All Bits solved")
|
|
1554
|
+
case i_t_status.VERIFICATION_FAILED:
|
|
1555
|
+
if isinstance(res_tbs := await Par2Data(self.par.image_transferred_blocks_status).exchange(c), result.Error):
|
|
1556
|
+
return res_tbs
|
|
1557
|
+
valid = tuple(res_tbs.value)
|
|
1558
|
+
c.log(logL.INFO, F"Got blocks[{len(res_tbs.value)}]") # todo: remove
|
|
1559
|
+
for i in filter(lambda it: valid[it] == b'\x00', range(len(valid))):
|
|
1560
|
+
offset = i * block_size
|
|
1561
|
+
if isinstance(res_tr_block := await TransferBlock(
|
|
1562
|
+
par=self.par,
|
|
1563
|
+
number=cdt.DoubleLongUnsigned(i),
|
|
1564
|
+
value=cdt.OctetString(bytearray(self.image[offset: offset + block_size]))
|
|
1565
|
+
).exchange(c), result.Error):
|
|
1566
|
+
return res_tr_block
|
|
1567
|
+
if isinstance(res_verify := await VerifyImage(self.par).exchange(c), result.Error):
|
|
1568
|
+
return res_verify
|
|
1569
|
+
c.log(logL.INFO, "Start Verify Transfer")
|
|
1570
|
+
case i_t_status.VERIFICATION_SUCCESSFUL:
|
|
1571
|
+
if isinstance(res_tbs := await Par2Data(self.par.image_transferred_blocks_status).exchange(c), result.Error):
|
|
1572
|
+
return res_tbs
|
|
1573
|
+
valid = tuple(res_tbs.value)
|
|
1574
|
+
if isinstance(res_ntb := await Par2Data(self.par.image_first_not_transferred_block_number).exchange(c), result.Error):
|
|
1575
|
+
return res_ntb
|
|
1576
|
+
self.n_t_b = int(res_ntb.value)
|
|
1577
|
+
if isinstance(res_activate_info := await Par2Data(self.par.image_to_activate_info).exchange(c), result.Error):
|
|
1578
|
+
return res_activate_info
|
|
1579
|
+
c.log(logL.INFO, F"md5:{res_activate_info.value[0].image_to_activate_signature}")
|
|
1580
|
+
if any(map(lambda it: it == b'\x00', valid)):
|
|
1581
|
+
if isinstance(res_initiate := await Execute2(self.par.image_transfer_initiate, self.ITI).exchange(c), result.Error):
|
|
1582
|
+
return res_initiate
|
|
1583
|
+
c.log(logL.INFO, "Start initiate Image Transfer, after wrong verify. Exist 0 blocks")
|
|
1584
|
+
return res.as_error(exc.DLMSException("Exist 0 blocks"))
|
|
1585
|
+
elif self.n_t_b < len(valid):
|
|
1586
|
+
if isinstance(res_initiate := await Execute2(self.par.image_transfer_initiate, self.ITI).exchange(c), result.Error):
|
|
1587
|
+
return res_initiate
|
|
1588
|
+
c.log(logL.INFO, "Start initiate Image Transfer, after wrong verify. Got not transferred block")
|
|
1589
|
+
return res.as_error(exc.DLMSException(F"Got {res_ntb.value} not transferred block"))
|
|
1590
|
+
elif res_activate_info.value[0].image_to_activate_signature != res_activate_info.value[0].image_to_activate_identification:
|
|
1591
|
+
if isinstance(res_initiate := await Execute2(self.par.image_transfer_initiate, self.ITI).exchange(c), result.Error):
|
|
1592
|
+
return res_initiate
|
|
1593
|
+
return res.as_error(exc.DLMSException(
|
|
1594
|
+
F"Signature not match to Identification: got {res_activate_info.value[0].image_to_activate_signature}, "
|
|
1595
|
+
F"expected {res_activate_info.value[0].image_to_activate_identification}"),
|
|
1596
|
+
"Start initiate Image Transfer, after wrong verify")
|
|
1597
|
+
else:
|
|
1598
|
+
if isinstance(res_activate := await ActivateImage(self.par).exchange(c), result.Error):
|
|
1599
|
+
return res_activate
|
|
1600
|
+
c.log(logL.INFO, "Start Activate Transfer")
|
|
1601
|
+
case i_t_status.ACTIVATION_INITIATED:
|
|
1602
|
+
try:
|
|
1603
|
+
await c.disconnect_request()
|
|
1604
|
+
except TimeoutError as e:
|
|
1605
|
+
c.log(logL.ERR, F"can't use <disconnect request>: {e}")
|
|
1606
|
+
if isinstance(res_reconnect := await HardwareReconnect(
|
|
1607
|
+
delay=self.waiting_for_activation,
|
|
1608
|
+
msg="expected reboot server after upgrade"
|
|
1609
|
+
).exchange(c), result.Error):
|
|
1610
|
+
return res_reconnect
|
|
1611
|
+
case i_t_status.ACTIVATION_SUCCESSFUL:
|
|
1612
|
+
if isinstance(res_activate_info := await Par2Data(self.par.image_to_activate_info).exchange(c), result.Error):
|
|
1613
|
+
return res_activate_info
|
|
1614
|
+
if res_activate_info.value[0].image_to_activate_identification == self.ITI.image_identifier:
|
|
1615
|
+
c.log(logL.INFO, "already activated this image")
|
|
1616
|
+
return res
|
|
1617
|
+
else:
|
|
1618
|
+
if isinstance(res_initiate := await Execute2(self.par.image_transfer_initiate, self.ITI).exchange(c), result.Error):
|
|
1619
|
+
return res_initiate
|
|
1620
|
+
c.log(logL.INFO, "Start initiate Image Transfer")
|
|
1621
|
+
# TODO: need wait clearing memory in device ~5 sec
|
|
1622
|
+
case i_t_status.ACTIVATION_FAILED:
|
|
1623
|
+
return res.as_error(exc.DLMSException(), "Ошибка активации...")
|
|
1624
|
+
case err:
|
|
1625
|
+
return res.as_error(exc.DLMSException(), f"Unknown image transfer status: {err}")
|
|
1626
|
+
previous_status = res_status.value
|
|
1627
|
+
await asyncio.sleep(1) # TODO: tune it
|
|
1628
|
+
if isinstance(res_status := await Par2Data(self.par.image_transfer_status).exchange(c), result.Error):
|
|
1629
|
+
return res_status
|
|
1630
|
+
c.log(logL.INFO, f"{res_status.value=}")
|
|
1631
|
+
|
|
1632
|
+
|
|
1633
|
+
@dataclass(frozen=True)
|
|
1634
|
+
class TransferBlock(SimpleCopy, OK):
|
|
1635
|
+
par: dlms_par.ImageTransfer
|
|
1636
|
+
number: cdt.DoubleLongUnsigned
|
|
1637
|
+
value: cdt.OctetString
|
|
1638
|
+
msg: str = "transfer block"
|
|
1639
|
+
|
|
1640
|
+
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1641
|
+
if isinstance(res := await Execute2(
|
|
1642
|
+
par=self.par.image_block_transfer,
|
|
1643
|
+
data=ImageBlockTransfer((self.number, self.value))
|
|
1644
|
+
).exchange(c), result.Error):
|
|
1645
|
+
return res
|
|
1646
|
+
return result.OK
|
|
1647
|
+
|
|
1648
|
+
|
|
1649
|
+
@dataclass(frozen=True)
|
|
1650
|
+
class VerifyImage(SimpleCopy, OK):
|
|
1651
|
+
par: dlms_par.ImageTransfer
|
|
1652
|
+
msg: str = "Verify image"
|
|
1653
|
+
|
|
1654
|
+
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1655
|
+
if isinstance(res := await Execute2(self.par.image_verify, integers.INTEGER_0).exchange(c), result.Error):
|
|
1656
|
+
return res
|
|
1657
|
+
return result.OK
|
|
1658
|
+
|
|
1659
|
+
|
|
1660
|
+
@dataclass(frozen=True)
|
|
1661
|
+
class ActivateImage(SimpleCopy, OK):
|
|
1662
|
+
par: dlms_par.ImageTransfer
|
|
1663
|
+
msg: str = "Activate image"
|
|
1664
|
+
|
|
1665
|
+
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1666
|
+
if isinstance(res := await Execute2(self.par.image_activate, integers.INTEGER_0).exchange(c), result.Error):
|
|
1667
|
+
return res
|
|
1668
|
+
return result.OK
|
|
1669
|
+
|
|
1670
|
+
|
|
1671
|
+
# todo: don't work with new API, remake
|
|
1672
|
+
class TestAll(OK):
|
|
1673
|
+
"""read all attributes with check access""" # todo: add Write with access
|
|
1674
|
+
msg: str = "test all"
|
|
1675
|
+
|
|
1676
|
+
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1677
|
+
# todo: refactoring with <append_err>
|
|
1678
|
+
res = result.ErrorAccumulator()
|
|
1679
|
+
if isinstance(res_objects := c.objects.sap2objects(c.SAP), result.Error):
|
|
1680
|
+
return res_objects
|
|
1681
|
+
ass: collection.AssociationLN = c.objects.sap2association(c.SAP)
|
|
1682
|
+
for obj in res_objects.value:
|
|
1683
|
+
indexes: list[int] = [i for i, _ in obj.get_index_with_attributes()]
|
|
1684
|
+
c.log(logL.INFO, F"start read {obj} attr: {', '.join(map(str, indexes))}")
|
|
1685
|
+
for i in indexes:
|
|
1686
|
+
is_readable = ass.is_readable(
|
|
1687
|
+
ln=obj.logical_name,
|
|
1688
|
+
index=i)
|
|
1689
|
+
if isinstance(res_read := await ReadObjAttr(obj, i).exchange(c), result.Error):
|
|
1690
|
+
if (
|
|
1691
|
+
res_read.has(pdu.DataAccessResult.READ_WRITE_DENIED, exc.ResultError)
|
|
1692
|
+
and not is_readable
|
|
1693
|
+
):
|
|
1694
|
+
c.log(logL.INFO, F"success ReadAccess TEST")
|
|
1695
|
+
else:
|
|
1696
|
+
res.append_err(res_read.err)
|
|
1697
|
+
elif not is_readable:
|
|
1698
|
+
res.append_e(PermissionError(f"{obj} with attr={i} must be unreadable"))
|
|
1699
|
+
indexes.remove(i)
|
|
1700
|
+
return res.result
|
|
1701
|
+
|
|
1702
|
+
|
|
1703
|
+
@dataclass
|
|
1704
|
+
class ApplyTemplate(SimpleCopy, Base):
|
|
1705
|
+
template: collection.Template
|
|
1706
|
+
msg: str = "apply template"
|
|
1707
|
+
|
|
1708
|
+
async def exchange(self, c: Client) -> result.Result:
|
|
1709
|
+
# todo: search col
|
|
1710
|
+
attr: cdt.CommonDataType
|
|
1711
|
+
res = result.StrictOk()
|
|
1712
|
+
for col in self.template.collections:
|
|
1713
|
+
if col == c.objects:
|
|
1714
|
+
use_col = col
|
|
1715
|
+
break
|
|
1716
|
+
else:
|
|
1717
|
+
c.log(logL.ERR, F"not find collection for {c}")
|
|
1718
|
+
raise asyncio.CancelledError()
|
|
1719
|
+
for ln, indexes in self.template.used.items():
|
|
1720
|
+
if (obj := res.propagate_err(use_col.logicalName2obj(ln))) is not None:
|
|
1721
|
+
for i in indexes:
|
|
1722
|
+
if (attr := obj.get_attr(i)) is not None:
|
|
1723
|
+
res.propagate_err(await WriteAttribute(
|
|
1724
|
+
ln=ln,
|
|
1725
|
+
index=i,
|
|
1726
|
+
value=attr.encoding
|
|
1727
|
+
).exchange(c))
|
|
1728
|
+
else:
|
|
1729
|
+
res.append_e(exc.EmptyObj(F"skip apply {self.template} {ln}:{i}: no value"))
|
|
1730
|
+
return res
|
|
1731
|
+
|
|
1732
|
+
|
|
1733
|
+
@dataclass
|
|
1734
|
+
class ReadTemplate(OK):
|
|
1735
|
+
template: collection.Template
|
|
1736
|
+
msg: str = "read template"
|
|
1737
|
+
|
|
1738
|
+
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1739
|
+
# todo: copypast from <ApplyTemplate>
|
|
1740
|
+
attr: cdt.CommonDataType
|
|
1741
|
+
res = result.ErrorAccumulator()
|
|
1742
|
+
for col in self.template.collections:
|
|
1743
|
+
if col == c._objects:
|
|
1744
|
+
use_col = col
|
|
1745
|
+
break
|
|
1746
|
+
else:
|
|
1747
|
+
c.log(logL.ERR, F"not find collection for {c}")
|
|
1748
|
+
raise asyncio.CancelledError()
|
|
1749
|
+
for ln, indexes in self.template.used.items():
|
|
1750
|
+
try:
|
|
1751
|
+
obj = use_col.get_object(ln) # todo: maybe not need
|
|
1752
|
+
except exc.NoObject as e:
|
|
1753
|
+
c.log(logL.WARN, F"skip apply {self.template}: {e}")
|
|
1754
|
+
continue
|
|
1755
|
+
res.propagate_err(
|
|
1756
|
+
await ReadAttributes(
|
|
1757
|
+
ln=ln,
|
|
1758
|
+
indexes=tuple(indexes)
|
|
1759
|
+
).exchange(c))
|
|
1760
|
+
return res.result
|
|
1761
|
+
|
|
1762
|
+
|
|
1763
|
+
@dataclass
|
|
1764
|
+
class AccessValidate(OK):
|
|
1765
|
+
"""check all access rights for current SAP"""
|
|
1766
|
+
with_correct: bool = False
|
|
1767
|
+
msg: str = "all access validate"
|
|
1768
|
+
|
|
1769
|
+
# todo: make with result.Error
|
|
1770
|
+
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1771
|
+
res = result.ErrorAccumulator()
|
|
1772
|
+
obj_l: ObjectListType
|
|
1773
|
+
el: ObjectListElement
|
|
1774
|
+
a_a_i: association_ln.abstract.AttributeAccessItem
|
|
1775
|
+
if (obj_l := c.objects.sap2association(c.SAP).object_list) is None:
|
|
1776
|
+
return result.Error.from_e(exc.EmptyObj(F"empty object_list for {c._objects.sap2association(c.SAP)}"))
|
|
1777
|
+
for el in obj_l:
|
|
1778
|
+
for a_a_i in el.access_rights.attribute_access:
|
|
1779
|
+
if a_a_i.access_mode.is_readable():
|
|
1780
|
+
i = int(a_a_i.attribute_id)
|
|
1781
|
+
if isinstance(res_read :=await ReadByDescriptor(ut.CosemAttributeDescriptor((
|
|
1782
|
+
int(el.class_id),
|
|
1783
|
+
el.logical_name.contents,
|
|
1784
|
+
i
|
|
1785
|
+
))).exchange(c), result.Error):
|
|
1786
|
+
res.append_err(res_read.err)
|
|
1787
|
+
if self.with_correct:
|
|
1788
|
+
a_a_i.access_mode.set(1) # todo: make better in future
|
|
1789
|
+
elif a_a_i.access_mode.is_writable():
|
|
1790
|
+
if isinstance(res_write :=await WriteAttribute(
|
|
1791
|
+
ln=el.logical_name,
|
|
1792
|
+
index=i,
|
|
1793
|
+
value=res_read.value
|
|
1794
|
+
).exchange(c), result.Error):
|
|
1795
|
+
res.append_err(res_write.err)
|
|
1796
|
+
return res.result
|
|
1797
|
+
|
|
1798
|
+
|
|
1799
|
+
@dataclass
|
|
1800
|
+
@deprecated("use <WriteList>")
|
|
1801
|
+
class WriteParDatas(SimpleCopy, _List[result.Ok]):
|
|
1802
|
+
"""write by ParData list"""
|
|
1803
|
+
par_datas: list[ParData]
|
|
1804
|
+
msg: str = ""
|
|
1805
|
+
|
|
1806
|
+
async def exchange(self, c: Client) -> result.List[result.Ok] | result.Error:
|
|
1807
|
+
res = result.List()
|
|
1808
|
+
for pardata in self.par_datas:
|
|
1809
|
+
res.append(await WriteAttribute(
|
|
1810
|
+
ln=pardata.par.ln,
|
|
1811
|
+
index=pardata.par.i,
|
|
1812
|
+
value=pardata.data.encoding
|
|
1813
|
+
).exchange(c))
|
|
1814
|
+
return res
|
|
1815
|
+
|
|
1816
|
+
|
|
1817
|
+
class WriteList(SimpleCopy, _List[result.Ok]):
|
|
1818
|
+
"""write by list"""
|
|
1819
|
+
par_datas: tuple[tuple[Parameter, cdt.CommonDataType], ...]
|
|
1820
|
+
err_ignore: bool
|
|
1821
|
+
|
|
1822
|
+
def __init__(self, *par_datas: tuple[Parameter, cdt.CommonDataType], err_ignore: bool = False, msg: str = "write list") -> None:
|
|
1823
|
+
self.par_datas = par_datas
|
|
1824
|
+
self.err_ignore = err_ignore
|
|
1825
|
+
self.msg = msg
|
|
1826
|
+
|
|
1827
|
+
async def exchange(self, c: Client) -> result.List[result.Ok] | result.Error:
|
|
1828
|
+
res = result.List[result.Ok]()
|
|
1829
|
+
for par, data in self.par_datas:
|
|
1830
|
+
if (
|
|
1831
|
+
isinstance(res_one := await Write2(par, data).exchange(c), result.Error)
|
|
1832
|
+
and not self.err_ignore
|
|
1833
|
+
):
|
|
1834
|
+
return res_one
|
|
1835
|
+
res.append(res_one)
|
|
1836
|
+
return res
|
|
1837
|
+
|
|
1838
|
+
|
|
1839
|
+
@dataclass(frozen=True)
|
|
1840
|
+
class WriteTranscript(SimpleCopy, OK):
|
|
1841
|
+
"""write by ParValues[Transcript]"""
|
|
1842
|
+
par: Parameter
|
|
1843
|
+
value: cdt.Transcript
|
|
1844
|
+
msg: str = "write with transcript"
|
|
1845
|
+
|
|
1846
|
+
async def exchange(self, c: Client) -> result.Ok | result.Error:
|
|
1847
|
+
if isinstance((res := await Par2Data[cdt.CommonDataType](self.par).exchange(c)), result.Error):
|
|
1848
|
+
return res
|
|
1849
|
+
if isinstance(res.value, cdt.Digital):
|
|
1850
|
+
s_u = c.objects.par2su(self.par)
|
|
1851
|
+
if isinstance(s_u, cdt.ScalUnitType):
|
|
1852
|
+
if not isinstance(self.value, str):
|
|
1853
|
+
return result.Error.from_e(TypeError(), f"for {self.par} got type: {self.value}, expected String")
|
|
1854
|
+
try:
|
|
1855
|
+
data = res.value.parse(value := str(float(self.value) * 10 ** -int(s_u.scaler)))
|
|
1856
|
+
except ValueError as e:
|
|
1857
|
+
return result.Error.from_e(e, f"for {self.par} got value: {self.value}, expected Float or Digital")
|
|
1858
|
+
else:
|
|
1859
|
+
data = res.value.parse(self.value)
|
|
1860
|
+
else:
|
|
1861
|
+
data = res.value.parse(self.value)
|
|
1862
|
+
return await Write2(self.par, data).exchange(c)
|
|
1863
|
+
|
|
1864
|
+
|
|
1865
|
+
class WriteTranscripts(SimpleCopy, _List[result.Ok]):
|
|
1866
|
+
"""write by ParValues[Transcript] list"""
|
|
1867
|
+
par_values: tuple[tuple[Parameter, cdt.Transcript], ...]
|
|
1868
|
+
err_ignore: bool
|
|
1869
|
+
|
|
1870
|
+
def __init__(self, *par_values: tuple[Parameter, cdt.Transcript], err_ignore: bool = False, msg: str ="write transcripts"):
|
|
1871
|
+
self.par_values = par_values
|
|
1872
|
+
self.err_ignore = err_ignore
|
|
1873
|
+
self.msg = msg
|
|
1874
|
+
|
|
1875
|
+
async def exchange(self, c: Client) -> result.List[result.Ok] | result.Error:
|
|
1876
|
+
res = result.List[result.Ok]()
|
|
1877
|
+
for par, value in self.par_values:
|
|
1878
|
+
if (
|
|
1879
|
+
isinstance(res_one := await WriteTranscript(par, value).exchange(c), result.Error)
|
|
1880
|
+
and not self.err_ignore
|
|
1881
|
+
):
|
|
1882
|
+
return res_one
|
|
1883
|
+
res.append(res_one)
|
|
1884
|
+
return res
|