pystuderxcom 3.4.0__tar.gz → 3.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pystuderxcom-3.4.0/src/pystuderxcom.egg-info → pystuderxcom-3.4.2}/PKG-INFO +1 -1
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/pyproject.toml +1 -1
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/src/pystuderxcom/__init__.py +1 -1
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/src/pystuderxcom/api_base_async.py +106 -17
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/src/pystuderxcom/api_base_sync.py +107 -18
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/src/pystuderxcom/const.py +8 -1
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/src/pystuderxcom/datapoints.py +14 -0
- pystuderxcom-3.4.2/src/pystuderxcom/datapoints_xcom.json +44 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/src/pystuderxcom/discover_async.py +5 -1
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/src/pystuderxcom/discover_sync.py +5 -1
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/src/pystuderxcom/factory_async.py +6 -2
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/src/pystuderxcom/factory_sync.py +6 -2
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/src/pystuderxcom/families.py +9 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2/src/pystuderxcom.egg-info}/PKG-INFO +1 -1
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/src/pystuderxcom.egg-info/SOURCES.txt +1 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/tests/test_api_base_async.py +165 -23
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/tests/test_api_base_sync.py +165 -23
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/tests/test_datapoints_async.py +20 -7
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/tests/test_datapoints_sync.py +20 -7
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/tests/test_discover_async.py +13 -13
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/tests/test_discover_sync.py +13 -13
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/tests/test_families.py +7 -1
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/LICENSE +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/README.md +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/setup.cfg +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/src/pystuderxcom/api_serial.py +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/src/pystuderxcom/api_tcp.py +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/src/pystuderxcom/api_udp.py +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/src/pystuderxcom/data.py +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/src/pystuderxcom/datapoints_120v.json +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/src/pystuderxcom/datapoints_240v.json +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/src/pystuderxcom/messages.py +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/src/pystuderxcom/messages_en.json +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/src/pystuderxcom/protocol.py +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/src/pystuderxcom/values.py +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/src/pystuderxcom.egg-info/dependency_links.txt +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/src/pystuderxcom.egg-info/requires.txt +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/src/pystuderxcom.egg-info/top_level.txt +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/tests/test_api_serial_async.py +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/tests/test_api_serial_sync.py +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/tests/test_api_tcp_async.py +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/tests/test_api_tcp_sync.py +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/tests/test_api_udp_async.py +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/tests/test_api_udp_sync.py +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/tests/test_const.py +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/tests/test_data.py +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/tests/test_messageset_async.py +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/tests/test_messageset_sync.py +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/tests/test_package_async.py +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/tests/test_package_sync.py +0 -0
- {pystuderxcom-3.4.0 → pystuderxcom-3.4.2}/tests/test_values.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pystuderxcom
|
|
3
|
-
Version: 3.4.
|
|
3
|
+
Version: 3.4.2
|
|
4
4
|
Summary: Python library to communicate with Studer-Innotec devices on the local network via Xcom protocol
|
|
5
5
|
Author-email: Anko Hanse <anko_hanse@hotmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/ankohanse/pystuderxcom
|
|
@@ -10,7 +10,7 @@ from .api_base_sync import XcomApiBase
|
|
|
10
10
|
from .discover_sync import XcomDiscover
|
|
11
11
|
from .factory_sync import XcomFactory
|
|
12
12
|
|
|
13
|
-
from .const import XcomApiTcpMode, XcomVoltage, XcomLevel, XcomFormat, XcomCategory, XcomAggregationType
|
|
13
|
+
from .const import XcomApiTcpMode, XcomVoltage, XcomLevel, XcomFormat, XcomTarget, XcomCategory, XcomAggregationType
|
|
14
14
|
from .const import XcomApiWriteException, XcomApiReadException, XcomApiTimeoutException, XcomApiUnpackException, XcomApiResponseIsError, XcomDiscoverNotConnected, XcomParamException
|
|
15
15
|
from .data import XcomDiscoveredClient, XcomDiscoveredDevice
|
|
16
16
|
from .datapoints import XcomDataset, XcomDatapoint, XcomDatapointUnknownException
|
|
@@ -14,6 +14,7 @@ from .const import (
|
|
|
14
14
|
REQ_BURST_PERIOD,
|
|
15
15
|
ScomAddress,
|
|
16
16
|
XcomFormat,
|
|
17
|
+
XcomTarget,
|
|
17
18
|
XcomCategory,
|
|
18
19
|
XcomAggregationType,
|
|
19
20
|
ScomFrameFlag,
|
|
@@ -196,6 +197,10 @@ class AsyncXcomApiBase:
|
|
|
196
197
|
XcomApiResponseIsError
|
|
197
198
|
"""
|
|
198
199
|
|
|
200
|
+
# Sanity check
|
|
201
|
+
if parameter.target != XcomTarget.STANDARD:
|
|
202
|
+
raise XcomParamException(f"Invalid datapoint passed to request_value; must have target STANDARD. Violated by datapoint '{parameter.name}' ({parameter.nr})")
|
|
203
|
+
|
|
199
204
|
# Check/convert input parameters
|
|
200
205
|
if type(dstAddr) is str:
|
|
201
206
|
dstAddr = XcomDeviceFamilies.get_addr_by_code(dstAddr)
|
|
@@ -220,6 +225,45 @@ class AsyncXcomApiBase:
|
|
|
220
225
|
msg = f"Failed to unpack response package for {parameter.nr}:{dstAddr}, data={response.frame_data.service_data.property_data.hex()}: {e}"
|
|
221
226
|
raise XcomApiUnpackException(msg) from None
|
|
222
227
|
|
|
228
|
+
|
|
229
|
+
async def request_virtual(self, parameter: XcomDatapoint, dstAddr = 100, retries = None, timeout = None, verbose=False):
|
|
230
|
+
"""
|
|
231
|
+
Request a virtual param or info.
|
|
232
|
+
Returns None if not connected, otherwise returns the requested value
|
|
233
|
+
Throws
|
|
234
|
+
XcomApiWriteException
|
|
235
|
+
XcomApiReadException
|
|
236
|
+
XcomApiTimeoutException
|
|
237
|
+
XcomApiResponseIsError
|
|
238
|
+
"""
|
|
239
|
+
# Sanity check
|
|
240
|
+
if not self.connected:
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
if parameter.target != XcomTarget.VIRTUAL:
|
|
244
|
+
raise XcomParamException(f"Invalid datapoint passed to request_value; must have target VIRTUAL. Violated by datapoint '{parameter.name}' ({parameter.nr})")
|
|
245
|
+
|
|
246
|
+
# Virtual request
|
|
247
|
+
match parameter.nr:
|
|
248
|
+
case 99000: return self.is_message_pending
|
|
249
|
+
case 99001: return self.was_rcc_reseted
|
|
250
|
+
case 99002: return self.is_sd_card_present
|
|
251
|
+
case 99003: return self.is_sd_card_full
|
|
252
|
+
case 99004: return self.is_new_datalogger_file_present
|
|
253
|
+
|
|
254
|
+
case 99020 | 99021 | 99022:
|
|
255
|
+
msg = await self.request_message(index=0, retries=retries, timeout=timeout, verbose=verbose)
|
|
256
|
+
if msg:
|
|
257
|
+
match parameter.nr:
|
|
258
|
+
case 99020: return msg.message_string
|
|
259
|
+
case 99021: return datetime.fromtimestamp(msg.timestamp)
|
|
260
|
+
case 99022: return msg.source_address
|
|
261
|
+
|
|
262
|
+
case _:
|
|
263
|
+
raise XcomParamException(f"Invalid datapoint passed to request_virtual; nr not recognized. Violated by datapoint '{parameter.name}' ({parameter.nr})")
|
|
264
|
+
|
|
265
|
+
return None
|
|
266
|
+
|
|
223
267
|
|
|
224
268
|
async def request_infos(self, request_data: XcomValues, retries = None, timeout = None, verbose=False) -> XcomValues:
|
|
225
269
|
"""
|
|
@@ -239,6 +283,9 @@ class AsyncXcomApiBase:
|
|
|
239
283
|
|
|
240
284
|
# Sanity check
|
|
241
285
|
for item in request_data.items:
|
|
286
|
+
if item.datapoint.target != XcomTarget.STANDARD:
|
|
287
|
+
raise XcomParamException(f"Invalid datapoint passed to request_infos; must have target STANDARD. Violated by datapoint '{item.datapoint.name}' ({item.datapoint.nr})")
|
|
288
|
+
|
|
242
289
|
if item.datapoint.category != XcomCategory.INFO:
|
|
243
290
|
raise XcomParamException(f"Invalid datapoint passed to request_infos; must have type INFO. Violated by datapoint '{item.datapoint.name}' ({item.datapoint.nr})")
|
|
244
291
|
|
|
@@ -268,12 +315,12 @@ class AsyncXcomApiBase:
|
|
|
268
315
|
|
|
269
316
|
async def request_values(self, request_data: XcomValues, retries = None, timeout = None, verbose=False) -> XcomValues:
|
|
270
317
|
"""
|
|
271
|
-
Request multiple infos
|
|
318
|
+
Request multiple infos, params or virtuals in one call.
|
|
272
319
|
Can only retrieve actual device values, NOT the average or sum over multiple devices.
|
|
273
320
|
|
|
274
321
|
The function will try to be as efficient as possible and combine retrieval of multiple infos in one call.
|
|
275
322
|
When the xcom-client does not support multiple-infos in one call, they are retried one by one.
|
|
276
|
-
Requested params are always retrieved one by one, so the function can take a while to finish.
|
|
323
|
+
Requested params and virtuals are always retrieved one by one, so the function can take a while to finish.
|
|
277
324
|
|
|
278
325
|
Returns None if not connected, otherwise returns the list of requested values
|
|
279
326
|
Throws
|
|
@@ -284,6 +331,7 @@ class AsyncXcomApiBase:
|
|
|
284
331
|
"""
|
|
285
332
|
|
|
286
333
|
# Sort out which XcomValues can be done via multi request_values and which must be done via single request_value
|
|
334
|
+
req_virtuals: list[XcomValuesItem] = []
|
|
287
335
|
req_singles: list[XcomValuesItem] = []
|
|
288
336
|
req_multi_items: list[XcomValuesItem] = []
|
|
289
337
|
req_multis: list[XcomValues] = []
|
|
@@ -291,26 +339,37 @@ class AsyncXcomApiBase:
|
|
|
291
339
|
|
|
292
340
|
for idx,item in enumerate(request_data.items):
|
|
293
341
|
|
|
294
|
-
match item.datapoint.
|
|
295
|
-
case
|
|
296
|
-
if item.
|
|
297
|
-
#
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
elif item.address is not None:
|
|
301
|
-
# Any others need to be done via an individual request_value cal
|
|
302
|
-
req_singles.append(item)
|
|
342
|
+
match item.datapoint.target:
|
|
343
|
+
case XcomTarget.VIRTUAL:
|
|
344
|
+
if item.address is not None:
|
|
345
|
+
# Needs to be done via an individual request_virtual call
|
|
346
|
+
req_virtuals.append(item)
|
|
303
347
|
|
|
304
348
|
else:
|
|
305
349
|
raise XcomParamException(f"Invalid XcomValuesItem passed to request_values; violated by code='{item.code}', address={item.address}, aggregation_type={item.aggregation_type}")
|
|
306
350
|
|
|
307
|
-
case
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
351
|
+
case XcomTarget.STANDARD:
|
|
352
|
+
# Standard datapoints are handled depending on category and aggregation type
|
|
353
|
+
match item.datapoint.category:
|
|
354
|
+
case XcomCategory.INFO:
|
|
355
|
+
if item.aggregation_type is not None and item.aggregation_type in range(XcomAggregationType.DEVICE1, XcomAggregationType.DEVICE15+1):
|
|
356
|
+
# Can be combined with other infos in a request_values call
|
|
357
|
+
req_multi_items.append(item)
|
|
311
358
|
|
|
312
|
-
|
|
313
|
-
|
|
359
|
+
elif item.address is not None:
|
|
360
|
+
# Any others need to be done via an individual request_value cal
|
|
361
|
+
req_singles.append(item)
|
|
362
|
+
|
|
363
|
+
else:
|
|
364
|
+
raise XcomParamException(f"Invalid XcomValuesItem passed to request_values; violated by code='{item.code}', address={item.address}, aggregation_type={item.aggregation_type}")
|
|
365
|
+
|
|
366
|
+
case XcomCategory.PARAMETER:
|
|
367
|
+
if item.address is not None:
|
|
368
|
+
# Needs to be done via an individual request_value call
|
|
369
|
+
req_singles.append(item)
|
|
370
|
+
|
|
371
|
+
else:
|
|
372
|
+
raise XcomParamException(f"Invalid XcomValuesItem passed to request_values; violated by code='{item.code}', address={item.address}, aggregation_type={item.aggregation_type}")
|
|
314
373
|
|
|
315
374
|
if (len(req_multi_items) == MULTI_INFO_REQ_MAX) or \
|
|
316
375
|
(len(req_multi_items) > 0 and idx == idx_last):
|
|
@@ -380,6 +439,36 @@ class AsyncXcomApiBase:
|
|
|
380
439
|
await asyncio.sleep(1)
|
|
381
440
|
burst_start = datetime.now()
|
|
382
441
|
|
|
442
|
+
# Finally perform all the virtual request_value requests
|
|
443
|
+
for req_virtual in req_virtuals:
|
|
444
|
+
try:
|
|
445
|
+
error = None
|
|
446
|
+
value = await self.request_virtual(req_virtual.datapoint, req_virtual.address, retries=retries, timeout=timeout, verbose=verbose)
|
|
447
|
+
|
|
448
|
+
except Exception as ex:
|
|
449
|
+
value = None
|
|
450
|
+
error = str(ex)
|
|
451
|
+
|
|
452
|
+
if error is not None:
|
|
453
|
+
_LOGGER.debug(f"Failed to retrieve virtual info or param {req_virtual.datapoint.nr}:{req_virtual.address}; {error}")
|
|
454
|
+
|
|
455
|
+
# Add to results
|
|
456
|
+
rsp_virtual = XcomValuesItem(
|
|
457
|
+
datapoint = req_virtual.datapoint,
|
|
458
|
+
code = req_virtual.code,
|
|
459
|
+
address = req_virtual.address,
|
|
460
|
+
aggregation_type=req_virtual.aggregation_type,
|
|
461
|
+
value = value,
|
|
462
|
+
error = error,
|
|
463
|
+
)
|
|
464
|
+
result_items.append(rsp_virtual)
|
|
465
|
+
|
|
466
|
+
# Periodically wait for a second. This will make sure we do not block Xcom-LAN with
|
|
467
|
+
# too many requests at once and prevent it from uploading data to the Studer portal.
|
|
468
|
+
if (datetime.now() - burst_start).total_seconds() > REQ_BURST_PERIOD:
|
|
469
|
+
await asyncio.sleep(1)
|
|
470
|
+
burst_start = datetime.now()
|
|
471
|
+
|
|
383
472
|
# Return all reponse items as one XcomValues object
|
|
384
473
|
return XcomValues(result_items)
|
|
385
474
|
|
|
@@ -16,6 +16,7 @@ from .const import (
|
|
|
16
16
|
REQ_BURST_PERIOD,
|
|
17
17
|
ScomAddress,
|
|
18
18
|
XcomFormat,
|
|
19
|
+
XcomTarget,
|
|
19
20
|
XcomCategory,
|
|
20
21
|
XcomAggregationType,
|
|
21
22
|
ScomFrameFlag,
|
|
@@ -58,8 +59,8 @@ from .values import (
|
|
|
58
59
|
XcomValues,
|
|
59
60
|
XcomValuesItem,
|
|
60
61
|
)
|
|
61
|
-
import time
|
|
62
62
|
import threading
|
|
63
|
+
import time
|
|
63
64
|
|
|
64
65
|
|
|
65
66
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -200,6 +201,10 @@ class XcomApiBase:
|
|
|
200
201
|
XcomApiResponseIsError
|
|
201
202
|
"""
|
|
202
203
|
|
|
204
|
+
# Sanity check
|
|
205
|
+
if parameter.target != XcomTarget.STANDARD:
|
|
206
|
+
raise XcomParamException(f"Invalid datapoint passed to request_value; must have target STANDARD. Violated by datapoint '{parameter.name}' ({parameter.nr})")
|
|
207
|
+
|
|
203
208
|
# Check/convert input parameters
|
|
204
209
|
if type(dstAddr) is str:
|
|
205
210
|
dstAddr = XcomDeviceFamilies.get_addr_by_code(dstAddr)
|
|
@@ -224,6 +229,45 @@ class XcomApiBase:
|
|
|
224
229
|
msg = f"Failed to unpack response package for {parameter.nr}:{dstAddr}, data={response.frame_data.service_data.property_data.hex()}: {e}"
|
|
225
230
|
raise XcomApiUnpackException(msg) from None
|
|
226
231
|
|
|
232
|
+
|
|
233
|
+
def request_virtual(self, parameter: XcomDatapoint, dstAddr = 100, retries = None, timeout = None, verbose=False):
|
|
234
|
+
"""
|
|
235
|
+
Request a virtual param or info.
|
|
236
|
+
Returns None if not connected, otherwise returns the requested value
|
|
237
|
+
Throws
|
|
238
|
+
XcomApiWriteException
|
|
239
|
+
XcomApiReadException
|
|
240
|
+
XcomApiTimeoutException
|
|
241
|
+
XcomApiResponseIsError
|
|
242
|
+
"""
|
|
243
|
+
# Sanity check
|
|
244
|
+
if not self.connected:
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
if parameter.target != XcomTarget.VIRTUAL:
|
|
248
|
+
raise XcomParamException(f"Invalid datapoint passed to request_value; must have target VIRTUAL. Violated by datapoint '{parameter.name}' ({parameter.nr})")
|
|
249
|
+
|
|
250
|
+
# Virtual request
|
|
251
|
+
match parameter.nr:
|
|
252
|
+
case 99000: return self.is_message_pending
|
|
253
|
+
case 99001: return self.was_rcc_reseted
|
|
254
|
+
case 99002: return self.is_sd_card_present
|
|
255
|
+
case 99003: return self.is_sd_card_full
|
|
256
|
+
case 99004: return self.is_new_datalogger_file_present
|
|
257
|
+
|
|
258
|
+
case 99020 | 99021 | 99022:
|
|
259
|
+
msg = self.request_message(index=0, retries=retries, timeout=timeout, verbose=verbose)
|
|
260
|
+
if msg:
|
|
261
|
+
match parameter.nr:
|
|
262
|
+
case 99020: return msg.message_string
|
|
263
|
+
case 99021: return datetime.fromtimestamp(msg.timestamp)
|
|
264
|
+
case 99022: return msg.source_address
|
|
265
|
+
|
|
266
|
+
case _:
|
|
267
|
+
raise XcomParamException(f"Invalid datapoint passed to request_virtual; nr not recognized. Violated by datapoint '{parameter.name}' ({parameter.nr})")
|
|
268
|
+
|
|
269
|
+
return None
|
|
270
|
+
|
|
227
271
|
|
|
228
272
|
def request_infos(self, request_data: XcomValues, retries = None, timeout = None, verbose=False) -> XcomValues:
|
|
229
273
|
"""
|
|
@@ -243,6 +287,9 @@ class XcomApiBase:
|
|
|
243
287
|
|
|
244
288
|
# Sanity check
|
|
245
289
|
for item in request_data.items:
|
|
290
|
+
if item.datapoint.target != XcomTarget.STANDARD:
|
|
291
|
+
raise XcomParamException(f"Invalid datapoint passed to request_infos; must have target STANDARD. Violated by datapoint '{item.datapoint.name}' ({item.datapoint.nr})")
|
|
292
|
+
|
|
246
293
|
if item.datapoint.category != XcomCategory.INFO:
|
|
247
294
|
raise XcomParamException(f"Invalid datapoint passed to request_infos; must have type INFO. Violated by datapoint '{item.datapoint.name}' ({item.datapoint.nr})")
|
|
248
295
|
|
|
@@ -272,12 +319,12 @@ class XcomApiBase:
|
|
|
272
319
|
|
|
273
320
|
def request_values(self, request_data: XcomValues, retries = None, timeout = None, verbose=False) -> XcomValues:
|
|
274
321
|
"""
|
|
275
|
-
Request multiple infos
|
|
322
|
+
Request multiple infos, params or virtuals in one call.
|
|
276
323
|
Can only retrieve actual device values, NOT the average or sum over multiple devices.
|
|
277
324
|
|
|
278
325
|
The function will try to be as efficient as possible and combine retrieval of multiple infos in one call.
|
|
279
326
|
When the xcom-client does not support multiple-infos in one call, they are retried one by one.
|
|
280
|
-
Requested params are always retrieved one by one, so the function can take a while to finish.
|
|
327
|
+
Requested params and virtuals are always retrieved one by one, so the function can take a while to finish.
|
|
281
328
|
|
|
282
329
|
Returns None if not connected, otherwise returns the list of requested values
|
|
283
330
|
Throws
|
|
@@ -288,6 +335,7 @@ class XcomApiBase:
|
|
|
288
335
|
"""
|
|
289
336
|
|
|
290
337
|
# Sort out which XcomValues can be done via multi request_values and which must be done via single request_value
|
|
338
|
+
req_virtuals: list[XcomValuesItem] = []
|
|
291
339
|
req_singles: list[XcomValuesItem] = []
|
|
292
340
|
req_multi_items: list[XcomValuesItem] = []
|
|
293
341
|
req_multis: list[XcomValues] = []
|
|
@@ -295,26 +343,37 @@ class XcomApiBase:
|
|
|
295
343
|
|
|
296
344
|
for idx,item in enumerate(request_data.items):
|
|
297
345
|
|
|
298
|
-
match item.datapoint.
|
|
299
|
-
case
|
|
300
|
-
if item.
|
|
301
|
-
#
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
elif item.address is not None:
|
|
305
|
-
# Any others need to be done via an individual request_value cal
|
|
306
|
-
req_singles.append(item)
|
|
346
|
+
match item.datapoint.target:
|
|
347
|
+
case XcomTarget.VIRTUAL:
|
|
348
|
+
if item.address is not None:
|
|
349
|
+
# Needs to be done via an individual request_virtual call
|
|
350
|
+
req_virtuals.append(item)
|
|
307
351
|
|
|
308
352
|
else:
|
|
309
353
|
raise XcomParamException(f"Invalid XcomValuesItem passed to request_values; violated by code='{item.code}', address={item.address}, aggregation_type={item.aggregation_type}")
|
|
310
354
|
|
|
311
|
-
case
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
355
|
+
case XcomTarget.STANDARD:
|
|
356
|
+
# Standard datapoints are handled depending on category and aggregation type
|
|
357
|
+
match item.datapoint.category:
|
|
358
|
+
case XcomCategory.INFO:
|
|
359
|
+
if item.aggregation_type is not None and item.aggregation_type in range(XcomAggregationType.DEVICE1, XcomAggregationType.DEVICE15+1):
|
|
360
|
+
# Can be combined with other infos in a request_values call
|
|
361
|
+
req_multi_items.append(item)
|
|
315
362
|
|
|
316
|
-
|
|
317
|
-
|
|
363
|
+
elif item.address is not None:
|
|
364
|
+
# Any others need to be done via an individual request_value cal
|
|
365
|
+
req_singles.append(item)
|
|
366
|
+
|
|
367
|
+
else:
|
|
368
|
+
raise XcomParamException(f"Invalid XcomValuesItem passed to request_values; violated by code='{item.code}', address={item.address}, aggregation_type={item.aggregation_type}")
|
|
369
|
+
|
|
370
|
+
case XcomCategory.PARAMETER:
|
|
371
|
+
if item.address is not None:
|
|
372
|
+
# Needs to be done via an individual request_value call
|
|
373
|
+
req_singles.append(item)
|
|
374
|
+
|
|
375
|
+
else:
|
|
376
|
+
raise XcomParamException(f"Invalid XcomValuesItem passed to request_values; violated by code='{item.code}', address={item.address}, aggregation_type={item.aggregation_type}")
|
|
318
377
|
|
|
319
378
|
if (len(req_multi_items) == MULTI_INFO_REQ_MAX) or \
|
|
320
379
|
(len(req_multi_items) > 0 and idx == idx_last):
|
|
@@ -384,6 +443,36 @@ class XcomApiBase:
|
|
|
384
443
|
time.sleep(1)
|
|
385
444
|
burst_start = datetime.now()
|
|
386
445
|
|
|
446
|
+
# Finally perform all the virtual request_value requests
|
|
447
|
+
for req_virtual in req_virtuals:
|
|
448
|
+
try:
|
|
449
|
+
error = None
|
|
450
|
+
value = self.request_virtual(req_virtual.datapoint, req_virtual.address, retries=retries, timeout=timeout, verbose=verbose)
|
|
451
|
+
|
|
452
|
+
except Exception as ex:
|
|
453
|
+
value = None
|
|
454
|
+
error = str(ex)
|
|
455
|
+
|
|
456
|
+
if error is not None:
|
|
457
|
+
_LOGGER.debug(f"Failed to retrieve virtual info or param {req_virtual.datapoint.nr}:{req_virtual.address}; {error}")
|
|
458
|
+
|
|
459
|
+
# Add to results
|
|
460
|
+
rsp_virtual = XcomValuesItem(
|
|
461
|
+
datapoint = req_virtual.datapoint,
|
|
462
|
+
code = req_virtual.code,
|
|
463
|
+
address = req_virtual.address,
|
|
464
|
+
aggregation_type=req_virtual.aggregation_type,
|
|
465
|
+
value = value,
|
|
466
|
+
error = error,
|
|
467
|
+
)
|
|
468
|
+
result_items.append(rsp_virtual)
|
|
469
|
+
|
|
470
|
+
# Periodically wait for a second. This will make sure we do not block Xcom-LAN with
|
|
471
|
+
# too many requests at once and prevent it from uploading data to the Studer portal.
|
|
472
|
+
if (datetime.now() - burst_start).total_seconds() > REQ_BURST_PERIOD:
|
|
473
|
+
time.sleep(1)
|
|
474
|
+
burst_start = datetime.now()
|
|
475
|
+
|
|
387
476
|
# Return all reponse items as one XcomValues object
|
|
388
477
|
return XcomValues(result_items)
|
|
389
478
|
|
|
@@ -37,6 +37,9 @@ REQ_TIMEOUT = 3
|
|
|
37
37
|
REQ_RETRIES = 3
|
|
38
38
|
REQ_BURST_PERIOD = 5 # do burst of requests for 5 seconds, then wait a second, then the next burst
|
|
39
39
|
|
|
40
|
+
NR_VIRTUAL_START = 98000
|
|
41
|
+
NR_VIRTUAL_END = 99999
|
|
42
|
+
|
|
40
43
|
|
|
41
44
|
class XcomApiTcpMode(StrEnum):
|
|
42
45
|
SERVER = "server"
|
|
@@ -187,6 +190,11 @@ class XcomMultiInfoFlags(IntEnum):
|
|
|
187
190
|
VT_PRESENT = 0x00000080,
|
|
188
191
|
VS_PRESENT = 0x00000100,
|
|
189
192
|
|
|
193
|
+
### values for target
|
|
194
|
+
class XcomTarget(StrEnum):
|
|
195
|
+
STANDARD = "standard"
|
|
196
|
+
VIRTUAL = "virtual"
|
|
197
|
+
|
|
190
198
|
### values for category
|
|
191
199
|
class XcomCategory:
|
|
192
200
|
INFO = 0x0001
|
|
@@ -207,7 +215,6 @@ class ScomObjType:
|
|
|
207
215
|
def __repr__(self):
|
|
208
216
|
return self.name
|
|
209
217
|
|
|
210
|
-
@staticmethod
|
|
211
218
|
@staticmethod
|
|
212
219
|
def from_str(s: str, default: int|None = None):
|
|
213
220
|
for e in XcomAggregationType:
|
|
@@ -9,9 +9,15 @@ import logging
|
|
|
9
9
|
from dataclasses import dataclass
|
|
10
10
|
|
|
11
11
|
from .const import (
|
|
12
|
+
NR_VIRTUAL_END,
|
|
13
|
+
NR_VIRTUAL_START,
|
|
12
14
|
XcomLevel,
|
|
13
15
|
XcomFormat,
|
|
14
16
|
XcomCategory,
|
|
17
|
+
XcomTarget,
|
|
18
|
+
)
|
|
19
|
+
from .families import (
|
|
20
|
+
XcomDeviceFamilies,
|
|
15
21
|
)
|
|
16
22
|
|
|
17
23
|
|
|
@@ -91,6 +97,13 @@ class XcomDatapoint:
|
|
|
91
97
|
_LOGGER.debug(f"Unknown category for datapoint {self.nr} with level {self.level} and format {self.format}")
|
|
92
98
|
return XcomCategory.INFO
|
|
93
99
|
|
|
100
|
+
@property
|
|
101
|
+
def target(self) -> XcomTarget:
|
|
102
|
+
if NR_VIRTUAL_START <= self.nr <= NR_VIRTUAL_END:
|
|
103
|
+
return XcomTarget.VIRTUAL
|
|
104
|
+
else:
|
|
105
|
+
return XcomTarget.STANDARD
|
|
106
|
+
|
|
94
107
|
def enum_value(self, key):
|
|
95
108
|
if self.format not in [XcomFormat.LONG_ENUM, XcomFormat.SHORT_ENUM]:
|
|
96
109
|
return None
|
|
@@ -117,6 +130,7 @@ class XcomDataset:
|
|
|
117
130
|
|
|
118
131
|
PATH_120V = __file__.replace('.py', '_120v.json')
|
|
119
132
|
PATH_240V = __file__.replace('.py', '_240v.json')
|
|
133
|
+
PATH_XCOM = __file__.replace('.py', '_xcom.json')
|
|
120
134
|
|
|
121
135
|
def __init__(self, datapoints: list[XcomDatapoint] | None = None):
|
|
122
136
|
self._datapoints = datapoints
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "_rem": "Extra 'virtual' Xcom datapoints"},
|
|
3
|
+
|
|
4
|
+
{ "_rem": "These are not included in 'Studer Technical Specification - Xtender serial protocol appendix'"},
|
|
5
|
+
{ "_rem": "But are defined by us based on other parameters exposed by the Xcom protocol"},
|
|
6
|
+
|
|
7
|
+
{ "_rem": "Column descriptions:"},
|
|
8
|
+
{ "_rem": " fam = Device Family"},
|
|
9
|
+
{ "_rem": " lvl = User level required to access the param or info"},
|
|
10
|
+
{ "_rem": " pnr = Parent number in menu structure"},
|
|
11
|
+
{ "_rem": " nr = Number of the param or info"},
|
|
12
|
+
{ "_rem": " name = Friendly name of the param or info"},
|
|
13
|
+
{ "_rem": " urcc = Data unit of measurement as displayed on RCC"},
|
|
14
|
+
{ "_rem": " unit = Data unit of measurement"},
|
|
15
|
+
{ "_rem": " def = Default value"},
|
|
16
|
+
{ "_rem": " min = Minimum value"},
|
|
17
|
+
{ "_rem": " max = Maximum value"},
|
|
18
|
+
{ "_rem": " fmt = Data format"},
|
|
19
|
+
{ "_rem": " inc = Increment step"},
|
|
20
|
+
{ "_rem": " opt = Options for selection in Enums"},
|
|
21
|
+
{ "_rem": " rem = Remarks"},
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
{ "_rem": "###"},
|
|
25
|
+
{ "_rem": "### Root Menu"},
|
|
26
|
+
{ "_rem": "###"},
|
|
27
|
+
{ "fam": "xcom", "lvl": "Info", "pnr": 0, "nr": 990, "name": "Xcom infos", "fmt": "ONLY LEVEL", "inc": "Menu" },
|
|
28
|
+
|
|
29
|
+
{ "_rem": "###"},
|
|
30
|
+
{ "_rem": "### Xcom specific params."},
|
|
31
|
+
{ "_rem": "###"},
|
|
32
|
+
{ "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99000, "name": "Is message pending", "short": "", "urcc": "", "unit": "", "fmt": "BOOL" },
|
|
33
|
+
{ "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99001, "name": "Was RCC reseted", "short": "", "urcc": "", "unit": "", "fmt": "BOOL" },
|
|
34
|
+
{ "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99002, "name": "Is sd-card present", "short": "", "urcc": "", "unit": "", "fmt": "BOOL" },
|
|
35
|
+
{ "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99003, "name": "Is sd-card full", "short": "", "urcc": "", "unit": "", "fmt": "BOOL" },
|
|
36
|
+
{ "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99004, "name": "Is new datalogger file present", "short": "", "urcc": "", "unit": "", "fmt": "BOOL" },
|
|
37
|
+
{ "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99020, "name": "Latest message", "short": "", "urcc": "", "unit": "", "fmt": "BOOL" },
|
|
38
|
+
{ "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99021, "name": "Latest message time", "short": "", "urcc": "", "unit": "", "fmt": "BOOL" },
|
|
39
|
+
{ "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99022, "name": "Latest message source", "short": "", "urcc": "", "unit": "", "fmt": "BOOL" },
|
|
40
|
+
|
|
41
|
+
{ "_rem": "###"},
|
|
42
|
+
{ "_rem": "### End of file"},
|
|
43
|
+
{ "_rem": "###"}
|
|
44
|
+
]
|
|
@@ -17,6 +17,7 @@ from .api_base_sync import (
|
|
|
17
17
|
)
|
|
18
18
|
from .const import (
|
|
19
19
|
XcomDiscoverNotConnected,
|
|
20
|
+
XcomTarget,
|
|
20
21
|
)
|
|
21
22
|
from .data import (
|
|
22
23
|
XcomDiscoveredDevice,
|
|
@@ -86,7 +87,10 @@ class AsyncXcomDiscover:
|
|
|
86
87
|
param = self._dataset.get_by_nr(nr, family.id_for_nr)
|
|
87
88
|
|
|
88
89
|
_LOGGER.info(f"Trying device {device_code} on {device_addr} for nr {nr}")
|
|
89
|
-
|
|
90
|
+
match param.target:
|
|
91
|
+
case XcomTarget.STANDARD: value = await self._api.request_value(param, device_addr, verbose=verbose)
|
|
92
|
+
case XcomTarget.VIRTUAL : value = await self._api.request_virtual(param, device_addr, verbose=verbose)
|
|
93
|
+
|
|
90
94
|
if value is not None:
|
|
91
95
|
_LOGGER.info(f" Found device {device_code} via {nr}:{device_addr}")
|
|
92
96
|
|
|
@@ -19,6 +19,7 @@ from .api_base_sync import (
|
|
|
19
19
|
)
|
|
20
20
|
from .const import (
|
|
21
21
|
XcomDiscoverNotConnected,
|
|
22
|
+
XcomTarget,
|
|
22
23
|
)
|
|
23
24
|
from .data import (
|
|
24
25
|
XcomDiscoveredDevice,
|
|
@@ -89,7 +90,10 @@ class XcomDiscover:
|
|
|
89
90
|
param = self._dataset.get_by_nr(nr, family.id_for_nr)
|
|
90
91
|
|
|
91
92
|
_LOGGER.info(f"Trying device {device_code} on {device_addr} for nr {nr}")
|
|
92
|
-
|
|
93
|
+
match param.target:
|
|
94
|
+
case XcomTarget.STANDARD: value = self._api.request_value(param, device_addr, verbose=verbose)
|
|
95
|
+
case XcomTarget.VIRTUAL : value = self._api.request_virtual(param, device_addr, verbose=verbose)
|
|
96
|
+
|
|
93
97
|
if value is not None:
|
|
94
98
|
_LOGGER.info(f" Found device {device_code} via {nr}:{device_addr}")
|
|
95
99
|
|
|
@@ -54,15 +54,19 @@ class AsyncXcomFactory:
|
|
|
54
54
|
text_120vac = await file_120vac.read()
|
|
55
55
|
async with aiofiles_open(XcomDataset.PATH_240V, "r", encoding="UTF-8") as file_240vac:
|
|
56
56
|
text_240vac = await file_240vac.read()
|
|
57
|
+
async with aiofiles_open(XcomDataset.PATH_XCOM, "r", encoding="UTF-8") as file_xcom:
|
|
58
|
+
text_xcom = await file_xcom.read()
|
|
57
59
|
|
|
58
60
|
values_120vac = orjson.loads(text_120vac)
|
|
59
61
|
values_240vac = orjson.loads(text_240vac)
|
|
62
|
+
values_xcom = orjson.loads(text_xcom)
|
|
60
63
|
|
|
61
64
|
datapoints_120vac = list(filter(None, [XcomDatapoint.from_dict(val) for val in values_120vac]))
|
|
62
65
|
datapoints_240vac = list(filter(None, [XcomDatapoint.from_dict(val) for val in values_240vac]))
|
|
66
|
+
datapoints_xcom = list(filter(None, [XcomDatapoint.from_dict(val) for val in values_xcom]))
|
|
63
67
|
|
|
64
|
-
# start with the 240v
|
|
65
|
-
datapoints = datapoints_240vac
|
|
68
|
+
# start with the merged 240v + xcom lists as base
|
|
69
|
+
datapoints = datapoints_240vac + datapoints_xcom
|
|
66
70
|
|
|
67
71
|
match voltageAC:
|
|
68
72
|
case XcomVoltage.AC240:
|
|
@@ -56,15 +56,19 @@ class XcomFactory:
|
|
|
56
56
|
text_120vac = file_120vac.read()
|
|
57
57
|
with open(XcomDataset.PATH_240V, "r", encoding="UTF-8") as file_240vac:
|
|
58
58
|
text_240vac = file_240vac.read()
|
|
59
|
+
with open(XcomDataset.PATH_XCOM, "r", encoding="UTF-8") as file_xcom:
|
|
60
|
+
text_xcom = file_xcom.read()
|
|
59
61
|
|
|
60
62
|
values_120vac = orjson.loads(text_120vac)
|
|
61
63
|
values_240vac = orjson.loads(text_240vac)
|
|
64
|
+
values_xcom = orjson.loads(text_xcom)
|
|
62
65
|
|
|
63
66
|
datapoints_120vac = list(filter(None, [XcomDatapoint.from_dict(val) for val in values_120vac]))
|
|
64
67
|
datapoints_240vac = list(filter(None, [XcomDatapoint.from_dict(val) for val in values_240vac]))
|
|
68
|
+
datapoints_xcom = list(filter(None, [XcomDatapoint.from_dict(val) for val in values_xcom]))
|
|
65
69
|
|
|
66
|
-
# start with the 240v
|
|
67
|
-
datapoints = datapoints_240vac
|
|
70
|
+
# start with the merged 240v + xcom lists as base
|
|
71
|
+
datapoints = datapoints_240vac + datapoints_xcom
|
|
68
72
|
|
|
69
73
|
match voltageAC:
|
|
70
74
|
case XcomVoltage.AC240:
|
|
@@ -137,6 +137,15 @@ class XcomDeviceFamilies:
|
|
|
137
137
|
15000, 15999, # nr for infos, start to end
|
|
138
138
|
15000, # nr for discovery
|
|
139
139
|
)
|
|
140
|
+
XCOM = XcomDeviceFamily(
|
|
141
|
+
"xcom", "xcom", # virtual device to expose additional values in a uniform way
|
|
142
|
+
"Xcom-LAN/232i",
|
|
143
|
+
990, # addr multicast to all devices (write only)
|
|
144
|
+
990, 990, # addr devices, start to end
|
|
145
|
+
98000, 98999, # nr for params, start to end
|
|
146
|
+
99000, 99999, # nr for infos, start to end
|
|
147
|
+
99000, # nr for discovery
|
|
148
|
+
)
|
|
140
149
|
|
|
141
150
|
|
|
142
151
|
@staticmethod
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pystuderxcom
|
|
3
|
-
Version: 3.4.
|
|
3
|
+
Version: 3.4.2
|
|
4
4
|
Summary: Python library to communicate with Studer-Innotec devices on the local network via Xcom protocol
|
|
5
5
|
Author-email: Anko Hanse <anko_hanse@hotmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/ankohanse/pystuderxcom
|
|
@@ -12,6 +12,7 @@ src/pystuderxcom/data.py
|
|
|
12
12
|
src/pystuderxcom/datapoints.py
|
|
13
13
|
src/pystuderxcom/datapoints_120v.json
|
|
14
14
|
src/pystuderxcom/datapoints_240v.json
|
|
15
|
+
src/pystuderxcom/datapoints_xcom.json
|
|
15
16
|
src/pystuderxcom/discover_async.py
|
|
16
17
|
src/pystuderxcom/discover_sync.py
|
|
17
18
|
src/pystuderxcom/factory_async.py
|