pystuderxcom 3.0.0__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.
- pystuderxcom/__init__.py +26 -0
- pystuderxcom/api_base_async.py +592 -0
- pystuderxcom/api_base_sync.py +596 -0
- pystuderxcom/api_serial.py +231 -0
- pystuderxcom/api_tcp.py +272 -0
- pystuderxcom/api_udp.py +234 -0
- pystuderxcom/const.py +293 -0
- pystuderxcom/data.py +325 -0
- pystuderxcom/datapoints.py +149 -0
- pystuderxcom/datapoints_120v.json +40 -0
- pystuderxcom/datapoints_240v.json +1531 -0
- pystuderxcom/discover_async.py +283 -0
- pystuderxcom/discover_sync.py +285 -0
- pystuderxcom/factory_async.py +138 -0
- pystuderxcom/factory_sync.py +140 -0
- pystuderxcom/families.py +269 -0
- pystuderxcom/messages.py +97 -0
- pystuderxcom/messages_en.json +210 -0
- pystuderxcom/protocol.py +247 -0
- pystuderxcom/values.py +139 -0
- pystuderxcom-3.0.0.dist-info/METADATA +152 -0
- pystuderxcom-3.0.0.dist-info/RECORD +25 -0
- pystuderxcom-3.0.0.dist-info/WHEEL +5 -0
- pystuderxcom-3.0.0.dist-info/licenses/LICENSE +674 -0
- pystuderxcom-3.0.0.dist-info/top_level.txt +1 -0
pystuderxcom/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from .api_tcp import AsyncXcomApiTcp, XcomApiTcp
|
|
2
|
+
from .api_udp import AsyncXcomApiUdp, XcomApiUdp
|
|
3
|
+
from .api_serial import AsyncXcomApiSerial, XcomApiSerial
|
|
4
|
+
|
|
5
|
+
from .api_base_async import AsyncXcomApiBase
|
|
6
|
+
from .discover_async import AsyncXcomDiscover
|
|
7
|
+
from .factory_async import AsyncXcomFactory
|
|
8
|
+
|
|
9
|
+
from .api_base_sync import XcomApiBase
|
|
10
|
+
from .discover_sync import XcomDiscover
|
|
11
|
+
from .factory_sync import XcomFactory
|
|
12
|
+
|
|
13
|
+
from .const import XcomVoltage, XcomLevel, XcomFormat, XcomCategory, XcomAggregationType
|
|
14
|
+
from .const import XcomApiWriteException, XcomApiReadException, XcomApiTimeoutException, XcomApiUnpackException, XcomApiResponseIsError, XcomDiscoverNotConnected, XcomParamException
|
|
15
|
+
from .data import XcomDiscoveredClient, XcomDiscoveredDevice
|
|
16
|
+
from .datapoints import XcomDataset, XcomDatapoint, XcomDatapointUnknownException
|
|
17
|
+
from .families import XcomDeviceFamily, XcomDeviceFamilies, XcomDeviceFamilyUnknownException, XcomDeviceCodeUnknownException, XcomDeviceAddrUnknownException
|
|
18
|
+
from .messages import XcomMessage, XcomMessageUnknownException
|
|
19
|
+
from .values import XcomValues, XcomValuesItem
|
|
20
|
+
|
|
21
|
+
# For unit testing
|
|
22
|
+
from .const import ScomObjType, ScomObjId, ScomService, ScomQspId, ScomQspLevel, ScomAddress, ScomErrorCode
|
|
23
|
+
from .data import XcomData, XcomDataMessageRsp, XcomDataMultiInfoReq, XcomDataMultiInfoReqItem, XcomDataMultiInfoRsp, XcomDataMultiInfoRspItem
|
|
24
|
+
from .messages import XcomMessageDef, XcomMessageSet
|
|
25
|
+
from .protocol import XcomHeader, XcomFrame, XcomService, XcomPackage
|
|
26
|
+
|
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
"""xcom_api.py: communication api to Studer Xcom via LAN."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import binascii
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
|
|
9
|
+
from .const import (
|
|
10
|
+
START_TIMEOUT,
|
|
11
|
+
STOP_TIMEOUT,
|
|
12
|
+
REQ_TIMEOUT,
|
|
13
|
+
REQ_RETRIES,
|
|
14
|
+
REQ_BURST_PERIOD,
|
|
15
|
+
ScomAddress,
|
|
16
|
+
XcomFormat,
|
|
17
|
+
XcomCategory,
|
|
18
|
+
XcomAggregationType,
|
|
19
|
+
ScomObjType,
|
|
20
|
+
ScomObjId,
|
|
21
|
+
ScomService,
|
|
22
|
+
ScomQspId,
|
|
23
|
+
XcomApiReadException,
|
|
24
|
+
XcomApiWriteException,
|
|
25
|
+
XcomApiUnpackException,
|
|
26
|
+
XcomApiTimeoutException,
|
|
27
|
+
XcomApiResponseIsError,
|
|
28
|
+
XcomParamException,
|
|
29
|
+
safe_len,
|
|
30
|
+
)
|
|
31
|
+
from .data import (
|
|
32
|
+
XcomData,
|
|
33
|
+
XcomDataMessageRsp,
|
|
34
|
+
MULTI_INFO_REQ_MAX,
|
|
35
|
+
)
|
|
36
|
+
from .datapoints import (
|
|
37
|
+
XcomDatapoint,
|
|
38
|
+
)
|
|
39
|
+
from .factory_async import (
|
|
40
|
+
AsyncXcomFactory,
|
|
41
|
+
)
|
|
42
|
+
from .factory_sync import (
|
|
43
|
+
XcomFactory,
|
|
44
|
+
)
|
|
45
|
+
from .families import (
|
|
46
|
+
XcomDeviceFamilies
|
|
47
|
+
)
|
|
48
|
+
from .messages import (
|
|
49
|
+
XcomMessage,
|
|
50
|
+
)
|
|
51
|
+
from .protocol import (
|
|
52
|
+
XcomPackage,
|
|
53
|
+
)
|
|
54
|
+
from .values import (
|
|
55
|
+
XcomValues,
|
|
56
|
+
XcomValuesItem,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
_LOGGER = logging.getLogger(__name__)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
##
|
|
64
|
+
## Base cass abstracting Xcom Api
|
|
65
|
+
##
|
|
66
|
+
class AsyncXcomApiBase:
|
|
67
|
+
|
|
68
|
+
def __init__(self):
|
|
69
|
+
"""
|
|
70
|
+
MOXA is connecting to the TCP Server we are creating here.
|
|
71
|
+
Once it is connected we can send package requests.
|
|
72
|
+
"""
|
|
73
|
+
self._connected = False
|
|
74
|
+
self._remote_ip = None
|
|
75
|
+
self._request_id = 0
|
|
76
|
+
self._sendRequestLock = asyncio.Lock() # to make sure _sendRequest_inner is never called concurrently
|
|
77
|
+
|
|
78
|
+
# Cached values
|
|
79
|
+
self._msg_set = None
|
|
80
|
+
|
|
81
|
+
# Diagnostics gathering
|
|
82
|
+
self._diag_retries = {}
|
|
83
|
+
self._diag_durations = {}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def start(self, timeout=START_TIMEOUT) -> bool:
|
|
87
|
+
"""
|
|
88
|
+
Start the Xcom Server and listening to the Xcom client.
|
|
89
|
+
"""
|
|
90
|
+
raise NotImplementedError()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def stop(self):
|
|
94
|
+
"""
|
|
95
|
+
Stop listening to the the Xcom Client and stop the Xcom Server.
|
|
96
|
+
"""
|
|
97
|
+
raise NotImplementedError()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def connected(self) -> bool:
|
|
102
|
+
"""Returns True if the Xcom client is connected, otherwise False"""
|
|
103
|
+
return self._connected
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def remote_ip(self) -> str|None:
|
|
108
|
+
"""Returns the IP address of the connected Xcom client, otherwise None"""
|
|
109
|
+
return self._remote_ip
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def _wait_until_connected(self, timeout) -> bool:
|
|
113
|
+
"""Wait for Xcom client to connect. Or timout."""
|
|
114
|
+
try:
|
|
115
|
+
for i in range(timeout):
|
|
116
|
+
if self._connected:
|
|
117
|
+
return True
|
|
118
|
+
|
|
119
|
+
await asyncio.sleep(1)
|
|
120
|
+
|
|
121
|
+
except Exception as e:
|
|
122
|
+
_LOGGER.warning(f"Exception while checking connection to Xcom client: {e}")
|
|
123
|
+
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
async def requestGuid(self, retries = None, timeout = None, verbose=False):
|
|
128
|
+
"""
|
|
129
|
+
Request the GUID that is used for remotely identifying the installation.
|
|
130
|
+
This function is only for the Modem or Ethernet mode
|
|
131
|
+
|
|
132
|
+
Returns None if not connected, otherwise returns the requested value
|
|
133
|
+
Throws
|
|
134
|
+
XcomApiWriteException
|
|
135
|
+
XcomApiReadException
|
|
136
|
+
XcomApiTimeoutException
|
|
137
|
+
XcomApiResponseIsError
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
# Compose the request and send it
|
|
141
|
+
request: XcomPackage = XcomPackage.genPackage(
|
|
142
|
+
service_id = ScomService.READ,
|
|
143
|
+
object_type = ScomObjType.GUID,
|
|
144
|
+
object_id = ScomObjId.NONE,
|
|
145
|
+
property_id = ScomQspId.NONE,
|
|
146
|
+
property_data = XcomData.NONE,
|
|
147
|
+
dst_addr = ScomAddress.RCC
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
response = await self._sendRequest(request, retries=retries, timeout=timeout, verbose=verbose)
|
|
151
|
+
if response is not None:
|
|
152
|
+
# Unpack the response value
|
|
153
|
+
try:
|
|
154
|
+
return XcomData.unpack(response.frame_data.service_data.property_data, XcomFormat.GUID)
|
|
155
|
+
|
|
156
|
+
except Exception as e:
|
|
157
|
+
msg = f"Failed to unpack response package for GUID:{request.header.dst_addr}, data={response.frame_data.service_data.property_data.hex()}: {e}"
|
|
158
|
+
raise XcomApiUnpackException(msg) from None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def requestValue(self, parameter: XcomDatapoint, dstAddr = 100, retries = None, timeout = None, verbose=False):
|
|
162
|
+
"""
|
|
163
|
+
Request a param or info.
|
|
164
|
+
Returns None if not connected, otherwise returns the requested value
|
|
165
|
+
Throws
|
|
166
|
+
XcomApiWriteException
|
|
167
|
+
XcomApiReadException
|
|
168
|
+
XcomApiTimeoutException
|
|
169
|
+
XcomApiResponseIsError
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
# Check/convert input parameters
|
|
173
|
+
if type(dstAddr) is str:
|
|
174
|
+
dstAddr = XcomDeviceFamilies.getAddrByCode(dstAddr)
|
|
175
|
+
|
|
176
|
+
# Compose the request and send it
|
|
177
|
+
request: XcomPackage = XcomPackage.genPackage(
|
|
178
|
+
service_id = ScomService.READ,
|
|
179
|
+
object_type = ScomObjType.PARAMETER if parameter.category == XcomCategory.PARAMETER else ScomObjType.INFO,
|
|
180
|
+
object_id = parameter.nr,
|
|
181
|
+
property_id = ScomQspId.UNSAVED_VALUE if parameter.category == XcomCategory.PARAMETER else ScomQspId.VALUE,
|
|
182
|
+
property_data = XcomData.NONE,
|
|
183
|
+
dst_addr = dstAddr
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
response = await self._sendRequest(request, retries=retries, timeout=timeout, verbose=verbose)
|
|
187
|
+
if response is not None:
|
|
188
|
+
# Unpack the response value
|
|
189
|
+
try:
|
|
190
|
+
return XcomData.unpack(response.frame_data.service_data.property_data, parameter.format)
|
|
191
|
+
|
|
192
|
+
except Exception as e:
|
|
193
|
+
msg = f"Failed to unpack response package for {parameter.nr}:{dstAddr}, data={response.frame_data.service_data.property_data.hex()}: {e}"
|
|
194
|
+
raise XcomApiUnpackException(msg) from None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
async def requestInfos(self, request_data: XcomValues, retries = None, timeout = None, verbose=False) -> XcomValues:
|
|
198
|
+
"""
|
|
199
|
+
Request multiple infos in one call.
|
|
200
|
+
Per info you can indicate what device to get it from, or to get Average or Sum of multiple devices
|
|
201
|
+
|
|
202
|
+
Returns None if not connected, otherwise returns the list of requested values
|
|
203
|
+
Throws
|
|
204
|
+
XcomApiWriteException
|
|
205
|
+
XcomApiReadException
|
|
206
|
+
XcomApiTimeoutException
|
|
207
|
+
XcomApiResponseIsError
|
|
208
|
+
|
|
209
|
+
Note: this requires at least firmware version 1.6.74 on your Xcom-232i/Xcom-LAN.
|
|
210
|
+
On older versions it results in a 'Service not supported' response from the Xcom client
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
# Sanity check
|
|
214
|
+
for item in request_data.items:
|
|
215
|
+
if item.datapoint.category != XcomCategory.INFO:
|
|
216
|
+
raise XcomParamException(f"Invalid datapoint passed to requestInfos; must have type INFO. Violated by datapoint '{item.datapoint.name}' ({item.datapoint.nr})")
|
|
217
|
+
|
|
218
|
+
if item.aggregation_type not in XcomAggregationType:
|
|
219
|
+
raise XcomParamException(f"Invalid aggregation_type passed to requestInfos; violated by '{item.aggregation_type}'")
|
|
220
|
+
|
|
221
|
+
# Compose the request and send it
|
|
222
|
+
request: XcomPackage = XcomPackage.genPackage(
|
|
223
|
+
service_id = ScomService.READ,
|
|
224
|
+
object_type = ScomObjType.MULTI_INFO,
|
|
225
|
+
object_id = ScomObjId.MULTI_INFO,
|
|
226
|
+
property_id = self._getNextRequestId() & 0xffff,
|
|
227
|
+
property_data = request_data.packRequest(),
|
|
228
|
+
dst_addr = ScomAddress.RCC
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
response = await self._sendRequest(request, retries=retries, timeout=timeout, verbose=verbose)
|
|
232
|
+
if response is not None:
|
|
233
|
+
try:
|
|
234
|
+
# Unpack the response value
|
|
235
|
+
return XcomValues.unpackResponse(response.frame_data.service_data.property_data, request_data)
|
|
236
|
+
|
|
237
|
+
except Exception as e:
|
|
238
|
+
msg = f"Failed to unpack response package for multi-info request, data={response.frame_data.service_data.property_data.hex()}: {e}"
|
|
239
|
+
raise XcomApiUnpackException(msg) from None
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
async def requestValues(self, request_data: XcomValues, retries = None, timeout = None, verbose=False) -> XcomValues:
|
|
243
|
+
"""
|
|
244
|
+
Request multiple infos and params in one call.
|
|
245
|
+
Can only retrieve actual device values, NOT the average or sum over multiple devices.
|
|
246
|
+
|
|
247
|
+
The function will try to be as efficient as possible and combine retrieval of multiple infos in one call.
|
|
248
|
+
When the xcom-client does not support multiple-infos in one call, they are retried one by one.
|
|
249
|
+
Requested params are always retrieved one by one, so the function can take a while to finish.
|
|
250
|
+
|
|
251
|
+
Returns None if not connected, otherwise returns the list of requested values
|
|
252
|
+
Throws
|
|
253
|
+
XcomApiWriteException
|
|
254
|
+
XcomApiReadException
|
|
255
|
+
XcomApiTimeoutException
|
|
256
|
+
XcomApiResponseIsError
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
# Sort out which XcomValues can be done via multi requestValues and which must be done via single requestValue
|
|
260
|
+
req_singles: list[XcomValuesItem] = []
|
|
261
|
+
req_multi_items: list[XcomValuesItem] = []
|
|
262
|
+
req_multis: list[XcomValues] = []
|
|
263
|
+
idx_last = safe_len(request_data.items)-1
|
|
264
|
+
|
|
265
|
+
for idx,item in enumerate(request_data.items):
|
|
266
|
+
|
|
267
|
+
match item.datapoint.category:
|
|
268
|
+
case XcomCategory.INFO:
|
|
269
|
+
if item.aggregation_type is not None and item.aggregation_type in range(XcomAggregationType.DEVICE1, XcomAggregationType.DEVICE15+1):
|
|
270
|
+
# Can be combined with other infos in a requestValues call
|
|
271
|
+
req_multi_items.append(item)
|
|
272
|
+
|
|
273
|
+
elif item.address is not None:
|
|
274
|
+
# Any others need to be done via an individual requestValue cal
|
|
275
|
+
req_singles.append(item)
|
|
276
|
+
|
|
277
|
+
else:
|
|
278
|
+
raise XcomParamException(f"Invalid XcomValuesItem passed to requestValues; violated by code='{item.code}', address={item.address}, aggregation_type={item.aggregation_type}")
|
|
279
|
+
|
|
280
|
+
case XcomCategory.PARAMETER:
|
|
281
|
+
if item.address is not None:
|
|
282
|
+
# Needs to be done via an individual requestValue call
|
|
283
|
+
req_singles.append(item)
|
|
284
|
+
|
|
285
|
+
else:
|
|
286
|
+
raise XcomParamException(f"Invalid XcomValuesItem passed to requestValues; violated by code='{item.code}', address={item.address}, aggregation_type={item.aggregation_type}")
|
|
287
|
+
|
|
288
|
+
if (len(req_multi_items) == MULTI_INFO_REQ_MAX) or \
|
|
289
|
+
(len(req_multi_items) > 0 and idx == idx_last):
|
|
290
|
+
|
|
291
|
+
# Start a new multi-items if current one if full or on last item of enumerate
|
|
292
|
+
req_multis.append( XcomValues(items=req_multi_items) )
|
|
293
|
+
req_multi_items = []
|
|
294
|
+
|
|
295
|
+
# Now perform all the multi requestValues requests
|
|
296
|
+
result_items: list[XcomValues] = []
|
|
297
|
+
burst_start = datetime.now()
|
|
298
|
+
|
|
299
|
+
for req_multi in req_multis:
|
|
300
|
+
try:
|
|
301
|
+
rsp_multi = await self.requestInfos(req_multi, retries=retries, timeout=timeout, verbose=verbose)
|
|
302
|
+
|
|
303
|
+
# Success; gather the returned response items
|
|
304
|
+
result_items.extend(rsp_multi.items)
|
|
305
|
+
|
|
306
|
+
except XcomApiTimeoutException as tex:
|
|
307
|
+
_LOGGER.debug(f"Failed to retrieve infos via single call. {tex}")
|
|
308
|
+
|
|
309
|
+
# Fail; do not retry as single requestValue also expected to give timeout
|
|
310
|
+
value = None
|
|
311
|
+
error = str(tex)
|
|
312
|
+
result_items.extend( [XcomValuesItem(req.datapoint, code=req.code, address=req.address, aggregation_type=req.aggregation_type, value=value, error=error) for req in req_multi.items] )
|
|
313
|
+
|
|
314
|
+
except Exception as ex:
|
|
315
|
+
_LOGGER.debug(f"Failed to retrieve infos via single call; will retry retrieve one-by-one. {ex}")
|
|
316
|
+
|
|
317
|
+
# Fail; retry all items as single requestValue
|
|
318
|
+
req_singles.extend(req_multi.items)
|
|
319
|
+
|
|
320
|
+
# Periodically wait for a second. This will make sure we do not block Xcom-LAN with
|
|
321
|
+
# too many requests at once and prevent it from uploading data to the Studer portal.
|
|
322
|
+
if (datetime.now() - burst_start).total_seconds() > REQ_BURST_PERIOD:
|
|
323
|
+
await asyncio.sleep(1)
|
|
324
|
+
burst_start = datetime.now()
|
|
325
|
+
|
|
326
|
+
# Next perform all the single requestValue requests
|
|
327
|
+
for req_single in req_singles:
|
|
328
|
+
try:
|
|
329
|
+
error = None
|
|
330
|
+
value = await self.requestValue(req_single.datapoint, req_single.address, retries=retries, timeout=timeout, verbose=verbose)
|
|
331
|
+
|
|
332
|
+
except Exception as ex:
|
|
333
|
+
value = None
|
|
334
|
+
error = str(ex)
|
|
335
|
+
|
|
336
|
+
if error is not None:
|
|
337
|
+
_LOGGER.debug(f"Failed to retrieve info or param {req_single.datapoint.nr}:{req_single.address}; {error}")
|
|
338
|
+
|
|
339
|
+
# Add to results
|
|
340
|
+
rsp_single = XcomValuesItem(
|
|
341
|
+
datapoint = req_single.datapoint,
|
|
342
|
+
code = req_single.code,
|
|
343
|
+
address = req_single.address,
|
|
344
|
+
aggregation_type=req_single.aggregation_type,
|
|
345
|
+
value = value,
|
|
346
|
+
error = error,
|
|
347
|
+
)
|
|
348
|
+
result_items.append(rsp_single)
|
|
349
|
+
|
|
350
|
+
# Periodically wait for a second. This will make sure we do not block Xcom-LAN with
|
|
351
|
+
# too many requests at once and prevent it from uploading data to the Studer portal.
|
|
352
|
+
if (datetime.now() - burst_start).total_seconds() > REQ_BURST_PERIOD:
|
|
353
|
+
await asyncio.sleep(1)
|
|
354
|
+
burst_start = datetime.now()
|
|
355
|
+
|
|
356
|
+
# Return all reponse items as one XcomValues object
|
|
357
|
+
return XcomValues(result_items)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
async def updateValue(self, parameter: XcomDatapoint, value, dstAddr = 100, retries = None, timeout = None, verbose=False):
|
|
361
|
+
"""
|
|
362
|
+
Update a param
|
|
363
|
+
Returns None if not connected, otherwise returns True on success
|
|
364
|
+
Throws
|
|
365
|
+
XcomApiWriteException
|
|
366
|
+
XcomApiReadException
|
|
367
|
+
XcomApiTimeoutException
|
|
368
|
+
XcomApiResponseIsError
|
|
369
|
+
"""
|
|
370
|
+
# Sanity check: the parameter/datapoint must have category == XcomDatapointType.PARAMETER
|
|
371
|
+
if parameter.category != XcomCategory.PARAMETER:
|
|
372
|
+
_LOGGER.warning(f"Ignoring attempt to update readonly infos value {parameter}")
|
|
373
|
+
return None
|
|
374
|
+
|
|
375
|
+
if type(dstAddr) is str:
|
|
376
|
+
dstAddr = XcomDeviceFamilies.getAddrByCode(dstAddr)
|
|
377
|
+
|
|
378
|
+
_LOGGER.debug(f"Update value {parameter} on addr {dstAddr}")
|
|
379
|
+
|
|
380
|
+
# Compose the request and send it
|
|
381
|
+
request: XcomPackage = XcomPackage.genPackage(
|
|
382
|
+
service_id = ScomService.WRITE,
|
|
383
|
+
object_type = ScomObjType.PARAMETER,
|
|
384
|
+
object_id = parameter.nr,
|
|
385
|
+
property_id = ScomQspId.UNSAVED_VALUE,
|
|
386
|
+
property_data = XcomData.pack(value, parameter.format),
|
|
387
|
+
dst_addr = dstAddr
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
response = await self._sendRequest(request, retries=retries, timeout=timeout, verbose=verbose)
|
|
391
|
+
if response is not None:
|
|
392
|
+
# No need to unpack the response value
|
|
393
|
+
return True
|
|
394
|
+
|
|
395
|
+
return False
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
async def requestMessage(self, index:int = 0, retries = None, timeout = None, verbose=False) -> XcomMessage:
|
|
399
|
+
"""
|
|
400
|
+
Request a Message from the RCC.
|
|
401
|
+
Reading a message with index 0 will return the last saved message in the flash memory of
|
|
402
|
+
the Xcom-232i and will also return the the number of remaining messages.
|
|
403
|
+
A side effect of reading a message with index 0 is that it will erase the flag informing
|
|
404
|
+
that there are new messages.
|
|
405
|
+
|
|
406
|
+
Reading a message with an index above 0 will return the message saved with that index.
|
|
407
|
+
|
|
408
|
+
Note: this requires at least firmware version 1.5.0 on your Xcom-232i/Xcom-LAN.
|
|
409
|
+
On older versions it results in a 'Service not supported' response from the Xcom client
|
|
410
|
+
|
|
411
|
+
Returns None if not connected, otherwise returns the requested value
|
|
412
|
+
Throws
|
|
413
|
+
XcomApiWriteException
|
|
414
|
+
XcomApiReadException
|
|
415
|
+
XcomApiTimeoutException
|
|
416
|
+
XcomApiResponseIsError
|
|
417
|
+
"""
|
|
418
|
+
|
|
419
|
+
# Make sure we have access to the message_set
|
|
420
|
+
if self._msg_set is None:
|
|
421
|
+
self._msg_set = await AsyncXcomFactory.create_messageset()
|
|
422
|
+
|
|
423
|
+
# Compose the request and send it
|
|
424
|
+
request: XcomPackage = XcomPackage.genPackage(
|
|
425
|
+
service_id = ScomService.READ,
|
|
426
|
+
object_type = ScomObjType.MESSAGE,
|
|
427
|
+
object_id = index,
|
|
428
|
+
property_id = ScomQspId.NONE,
|
|
429
|
+
property_data = XcomData.NONE,
|
|
430
|
+
dst_addr = ScomAddress.RCC,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
response = await self._sendRequest(request, retries=retries, timeout=timeout, verbose=verbose)
|
|
434
|
+
if response is not None:
|
|
435
|
+
# Unpack the response value
|
|
436
|
+
try:
|
|
437
|
+
# Unpack the response value
|
|
438
|
+
rsp_data = XcomDataMessageRsp.unpack(response.frame_data.service_data.property_data)
|
|
439
|
+
return XcomMessage(rsp_data, self._msg_set)
|
|
440
|
+
|
|
441
|
+
except Exception as e:
|
|
442
|
+
msg = f"Failed to unpack response package for message request, data={response.frame_data.service_data.property_data.hex()}: {e}"
|
|
443
|
+
raise XcomApiUnpackException(msg) from None
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
async def _sendRequest(self, request: XcomPackage, retries = None, timeout = None, verbose=False):
|
|
447
|
+
|
|
448
|
+
# Sometimes the Xcom client does not seem to pickup a request
|
|
449
|
+
# so retry if needed
|
|
450
|
+
if not self._connected:
|
|
451
|
+
_LOGGER.warning(f"_sendRequest - not connected")
|
|
452
|
+
return None
|
|
453
|
+
|
|
454
|
+
last_exception = None
|
|
455
|
+
retries = retries or REQ_RETRIES
|
|
456
|
+
timeout = timeout or REQ_TIMEOUT
|
|
457
|
+
|
|
458
|
+
for retry in range(retries):
|
|
459
|
+
try:
|
|
460
|
+
async with self._sendRequestLock:
|
|
461
|
+
ts_start = datetime.now()
|
|
462
|
+
|
|
463
|
+
response = await self._sendRequest_inner(request, timeout=timeout, verbose=verbose)
|
|
464
|
+
|
|
465
|
+
# Update diagnostics
|
|
466
|
+
ts_end = datetime.now()
|
|
467
|
+
await self._addDiagnostics(retries = retry, duration = ts_end-ts_start)
|
|
468
|
+
|
|
469
|
+
# Check the response
|
|
470
|
+
if response is None:
|
|
471
|
+
return None
|
|
472
|
+
|
|
473
|
+
if response.isError():
|
|
474
|
+
raise XcomApiResponseIsError(response.getError())
|
|
475
|
+
|
|
476
|
+
# Success
|
|
477
|
+
return response
|
|
478
|
+
|
|
479
|
+
except Exception as e:
|
|
480
|
+
last_exception = e
|
|
481
|
+
|
|
482
|
+
# Update diagnostics in case of timeout of each retry
|
|
483
|
+
await self._addDiagnostics(retries = retry)
|
|
484
|
+
|
|
485
|
+
if last_exception:
|
|
486
|
+
raise last_exception from None
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
async def _sendRequest_inner(self, request: XcomPackage, retries = None, timeout = None, verbose=False):
|
|
490
|
+
"""
|
|
491
|
+
Send a request package to the Xcom client and wait for the correct response package
|
|
492
|
+
"""
|
|
493
|
+
|
|
494
|
+
# Send the request package to the Xcom client
|
|
495
|
+
try:
|
|
496
|
+
if verbose:
|
|
497
|
+
data = request.getBytes()
|
|
498
|
+
_LOGGER.debug(f"send {len(data)} bytes ({binascii.hexlify(data).decode('ascii')}), decoded: {request}")
|
|
499
|
+
|
|
500
|
+
await self._sendPackage(request)
|
|
501
|
+
|
|
502
|
+
except Exception as e:
|
|
503
|
+
msg = f"Exception while sending request package to Xcom client: {e}"
|
|
504
|
+
raise XcomApiWriteException(msg) from None
|
|
505
|
+
|
|
506
|
+
# Receive packages until we get the one we expect
|
|
507
|
+
# We implement our own primitive timeout mechanism that
|
|
508
|
+
# is robust when converted from async to sync via unasyncd tool
|
|
509
|
+
ts_end = datetime.now() + timedelta(seconds=timeout)
|
|
510
|
+
|
|
511
|
+
while datetime.now() < ts_end:
|
|
512
|
+
try:
|
|
513
|
+
response = await self._receivePackage()
|
|
514
|
+
|
|
515
|
+
if response is None:
|
|
516
|
+
continue
|
|
517
|
+
|
|
518
|
+
if response.isResponse() and \
|
|
519
|
+
response.frame_data.service_id == request.frame_data.service_id and \
|
|
520
|
+
response.frame_data.service_data.object_id == request.frame_data.service_data.object_id and \
|
|
521
|
+
response.frame_data.service_data.property_id == request.frame_data.service_data.property_id:
|
|
522
|
+
|
|
523
|
+
# Yes, this is the answer to our request
|
|
524
|
+
if verbose:
|
|
525
|
+
data = response.getBytes()
|
|
526
|
+
_LOGGER.debug(f"recv {len(data)} bytes ({binascii.hexlify(data).decode('ascii')}), decoded: {response}")
|
|
527
|
+
|
|
528
|
+
return response
|
|
529
|
+
else:
|
|
530
|
+
# No, not an answer to our request, continue loop for next answer (or timeout)
|
|
531
|
+
if verbose:
|
|
532
|
+
data = response.getBytes()
|
|
533
|
+
_LOGGER.debug(f"skip {len(data)} bytes ({binascii.hexlify(data).decode('ascii')}), decoded: {response}")
|
|
534
|
+
|
|
535
|
+
except Exception as e:
|
|
536
|
+
msg = f"Exception while listening for response package from Xcom client: {e}"
|
|
537
|
+
raise XcomApiReadException() from None
|
|
538
|
+
|
|
539
|
+
# If we reach this point then there was a timeout
|
|
540
|
+
msg = f"Timeout while listening for response package from Xcom client"
|
|
541
|
+
raise XcomApiTimeoutException(msg) from None
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
async def _sendPackage(self, package: XcomPackage):
|
|
545
|
+
"""
|
|
546
|
+
Send an Xcom package.
|
|
547
|
+
Exception handling is dealed with by the caller.
|
|
548
|
+
|
|
549
|
+
Must be implemented in derived classes.
|
|
550
|
+
"""
|
|
551
|
+
raise NotImplementedError()
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
async def _receivePackage(self) -> XcomPackage | None:
|
|
555
|
+
"""
|
|
556
|
+
Attempt to receive an Xcom package.
|
|
557
|
+
Return None of nothing was received within REQ_TIMEOUT.
|
|
558
|
+
Exception handling is dealed with by the caller.
|
|
559
|
+
|
|
560
|
+
Must be implemented in derived classes.
|
|
561
|
+
"""
|
|
562
|
+
raise NotImplementedError()
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _getNextRequestId(self) -> int:
|
|
566
|
+
self._request_id += 1
|
|
567
|
+
return self._request_id
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
async def _addDiagnostics(self, retries: int = None, duration: timedelta = None):
|
|
571
|
+
if retries is not None:
|
|
572
|
+
if retries not in self._diag_retries:
|
|
573
|
+
self._diag_retries[retries] = 1
|
|
574
|
+
else:
|
|
575
|
+
self._diag_retries[retries] += 1
|
|
576
|
+
|
|
577
|
+
if duration is not None:
|
|
578
|
+
duration = round(duration.total_seconds(), 1)
|
|
579
|
+
if duration not in self._diag_durations:
|
|
580
|
+
self._diag_durations[duration] = 1
|
|
581
|
+
else:
|
|
582
|
+
self._diag_durations[duration] += 1
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
async def getDiagnostics(self):
|
|
586
|
+
return {
|
|
587
|
+
"statistics": {
|
|
588
|
+
"retries": dict(sorted(self._diag_retries.items())),
|
|
589
|
+
"durations": dict(sorted(self._diag_durations.items())),
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|