pystuderxcom 3.4.1__tar.gz → 3.4.3__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.1/src/pystuderxcom.egg-info → pystuderxcom-3.4.3}/PKG-INFO +1 -1
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/pyproject.toml +1 -1
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/__init__.py +1 -1
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/api_base_async.py +74 -22
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/api_base_sync.py +75 -23
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/const.py +8 -2
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/datapoints.py +13 -3
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/datapoints_xcom.json +11 -8
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/discover_async.py +5 -1
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/discover_sync.py +5 -1
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/families.py +2 -2
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3/src/pystuderxcom.egg-info}/PKG-INFO +1 -1
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_api_base_async.py +112 -18
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_api_base_sync.py +112 -18
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_datapoints_async.py +8 -1
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_datapoints_sync.py +8 -1
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/LICENSE +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/README.md +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/setup.cfg +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/api_serial.py +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/api_tcp.py +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/api_udp.py +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/data.py +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/datapoints_120v.json +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/datapoints_240v.json +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/factory_async.py +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/factory_sync.py +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/messages.py +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/messages_en.json +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/protocol.py +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/values.py +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom.egg-info/SOURCES.txt +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom.egg-info/dependency_links.txt +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom.egg-info/requires.txt +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom.egg-info/top_level.txt +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_api_serial_async.py +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_api_serial_sync.py +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_api_tcp_async.py +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_api_tcp_sync.py +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_api_udp_async.py +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_api_udp_sync.py +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_const.py +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_data.py +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_discover_async.py +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_discover_sync.py +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_families.py +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_messageset_async.py +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_messageset_sync.py +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_package_async.py +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_package_sync.py +0 -0
- {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/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.3
|
|
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,14 +197,14 @@ 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)
|
|
202
207
|
|
|
203
|
-
# Check for virtual value request
|
|
204
|
-
if parameter.category in [XcomCategory.VIRTUAL]:
|
|
205
|
-
return await self.request_virtual(parameter, dstAddr=dstAddr, retries=retries, timeout=timeout, verbose=verbose)
|
|
206
|
-
|
|
207
208
|
# Compose the request and send it
|
|
208
209
|
request: XcomPackage = XcomPackage.gen_package(
|
|
209
210
|
service_id = ScomServiceId.READ,
|
|
@@ -235,9 +236,12 @@ class AsyncXcomApiBase:
|
|
|
235
236
|
XcomApiTimeoutException
|
|
236
237
|
XcomApiResponseIsError
|
|
237
238
|
"""
|
|
238
|
-
|
|
239
|
+
# Sanity check
|
|
239
240
|
if not self.connected:
|
|
240
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})")
|
|
241
245
|
|
|
242
246
|
# Virtual request
|
|
243
247
|
match parameter.nr:
|
|
@@ -255,6 +259,9 @@ class AsyncXcomApiBase:
|
|
|
255
259
|
case 99021: return datetime.fromtimestamp(msg.timestamp)
|
|
256
260
|
case 99022: return msg.source_address
|
|
257
261
|
|
|
262
|
+
case _:
|
|
263
|
+
raise XcomParamException(f"Invalid datapoint passed to request_virtual; nr not recognized. Violated by datapoint '{parameter.name}' ({parameter.nr})")
|
|
264
|
+
|
|
258
265
|
return None
|
|
259
266
|
|
|
260
267
|
|
|
@@ -276,6 +283,9 @@ class AsyncXcomApiBase:
|
|
|
276
283
|
|
|
277
284
|
# Sanity check
|
|
278
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
|
+
|
|
279
289
|
if item.datapoint.category != XcomCategory.INFO:
|
|
280
290
|
raise XcomParamException(f"Invalid datapoint passed to request_infos; must have type INFO. Violated by datapoint '{item.datapoint.name}' ({item.datapoint.nr})")
|
|
281
291
|
|
|
@@ -305,12 +315,12 @@ class AsyncXcomApiBase:
|
|
|
305
315
|
|
|
306
316
|
async def request_values(self, request_data: XcomValues, retries = None, timeout = None, verbose=False) -> XcomValues:
|
|
307
317
|
"""
|
|
308
|
-
Request multiple infos
|
|
318
|
+
Request multiple infos, params or virtuals in one call.
|
|
309
319
|
Can only retrieve actual device values, NOT the average or sum over multiple devices.
|
|
310
320
|
|
|
311
321
|
The function will try to be as efficient as possible and combine retrieval of multiple infos in one call.
|
|
312
322
|
When the xcom-client does not support multiple-infos in one call, they are retried one by one.
|
|
313
|
-
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.
|
|
314
324
|
|
|
315
325
|
Returns None if not connected, otherwise returns the list of requested values
|
|
316
326
|
Throws
|
|
@@ -321,6 +331,7 @@ class AsyncXcomApiBase:
|
|
|
321
331
|
"""
|
|
322
332
|
|
|
323
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] = []
|
|
324
335
|
req_singles: list[XcomValuesItem] = []
|
|
325
336
|
req_multi_items: list[XcomValuesItem] = []
|
|
326
337
|
req_multis: list[XcomValues] = []
|
|
@@ -328,26 +339,37 @@ class AsyncXcomApiBase:
|
|
|
328
339
|
|
|
329
340
|
for idx,item in enumerate(request_data.items):
|
|
330
341
|
|
|
331
|
-
match item.datapoint.
|
|
332
|
-
case
|
|
333
|
-
if item.
|
|
334
|
-
#
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
elif item.address is not None:
|
|
338
|
-
# Any others need to be done via an individual request_value cal
|
|
339
|
-
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)
|
|
340
347
|
|
|
341
348
|
else:
|
|
342
349
|
raise XcomParamException(f"Invalid XcomValuesItem passed to request_values; violated by code='{item.code}', address={item.address}, aggregation_type={item.aggregation_type}")
|
|
343
350
|
|
|
344
|
-
case
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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)
|
|
348
358
|
|
|
349
|
-
|
|
350
|
-
|
|
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}")
|
|
351
373
|
|
|
352
374
|
if (len(req_multi_items) == MULTI_INFO_REQ_MAX) or \
|
|
353
375
|
(len(req_multi_items) > 0 and idx == idx_last):
|
|
@@ -417,6 +439,36 @@ class AsyncXcomApiBase:
|
|
|
417
439
|
await asyncio.sleep(1)
|
|
418
440
|
burst_start = datetime.now()
|
|
419
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
|
+
|
|
420
472
|
# Return all reponse items as one XcomValues object
|
|
421
473
|
return XcomValues(result_items)
|
|
422
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,14 +201,14 @@ 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)
|
|
206
211
|
|
|
207
|
-
# Check for virtual value request
|
|
208
|
-
if parameter.category in [XcomCategory.VIRTUAL]:
|
|
209
|
-
return self.request_virtual(parameter, dstAddr=dstAddr, retries=retries, timeout=timeout, verbose=verbose)
|
|
210
|
-
|
|
211
212
|
# Compose the request and send it
|
|
212
213
|
request: XcomPackage = XcomPackage.gen_package(
|
|
213
214
|
service_id = ScomServiceId.READ,
|
|
@@ -239,9 +240,12 @@ class XcomApiBase:
|
|
|
239
240
|
XcomApiTimeoutException
|
|
240
241
|
XcomApiResponseIsError
|
|
241
242
|
"""
|
|
242
|
-
|
|
243
|
+
# Sanity check
|
|
243
244
|
if not self.connected:
|
|
244
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})")
|
|
245
249
|
|
|
246
250
|
# Virtual request
|
|
247
251
|
match parameter.nr:
|
|
@@ -259,6 +263,9 @@ class XcomApiBase:
|
|
|
259
263
|
case 99021: return datetime.fromtimestamp(msg.timestamp)
|
|
260
264
|
case 99022: return msg.source_address
|
|
261
265
|
|
|
266
|
+
case _:
|
|
267
|
+
raise XcomParamException(f"Invalid datapoint passed to request_virtual; nr not recognized. Violated by datapoint '{parameter.name}' ({parameter.nr})")
|
|
268
|
+
|
|
262
269
|
return None
|
|
263
270
|
|
|
264
271
|
|
|
@@ -280,6 +287,9 @@ class XcomApiBase:
|
|
|
280
287
|
|
|
281
288
|
# Sanity check
|
|
282
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
|
+
|
|
283
293
|
if item.datapoint.category != XcomCategory.INFO:
|
|
284
294
|
raise XcomParamException(f"Invalid datapoint passed to request_infos; must have type INFO. Violated by datapoint '{item.datapoint.name}' ({item.datapoint.nr})")
|
|
285
295
|
|
|
@@ -309,12 +319,12 @@ class XcomApiBase:
|
|
|
309
319
|
|
|
310
320
|
def request_values(self, request_data: XcomValues, retries = None, timeout = None, verbose=False) -> XcomValues:
|
|
311
321
|
"""
|
|
312
|
-
Request multiple infos
|
|
322
|
+
Request multiple infos, params or virtuals in one call.
|
|
313
323
|
Can only retrieve actual device values, NOT the average or sum over multiple devices.
|
|
314
324
|
|
|
315
325
|
The function will try to be as efficient as possible and combine retrieval of multiple infos in one call.
|
|
316
326
|
When the xcom-client does not support multiple-infos in one call, they are retried one by one.
|
|
317
|
-
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.
|
|
318
328
|
|
|
319
329
|
Returns None if not connected, otherwise returns the list of requested values
|
|
320
330
|
Throws
|
|
@@ -325,6 +335,7 @@ class XcomApiBase:
|
|
|
325
335
|
"""
|
|
326
336
|
|
|
327
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] = []
|
|
328
339
|
req_singles: list[XcomValuesItem] = []
|
|
329
340
|
req_multi_items: list[XcomValuesItem] = []
|
|
330
341
|
req_multis: list[XcomValues] = []
|
|
@@ -332,26 +343,37 @@ class XcomApiBase:
|
|
|
332
343
|
|
|
333
344
|
for idx,item in enumerate(request_data.items):
|
|
334
345
|
|
|
335
|
-
match item.datapoint.
|
|
336
|
-
case
|
|
337
|
-
if item.
|
|
338
|
-
#
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
elif item.address is not None:
|
|
342
|
-
# Any others need to be done via an individual request_value cal
|
|
343
|
-
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)
|
|
344
351
|
|
|
345
352
|
else:
|
|
346
353
|
raise XcomParamException(f"Invalid XcomValuesItem passed to request_values; violated by code='{item.code}', address={item.address}, aggregation_type={item.aggregation_type}")
|
|
347
354
|
|
|
348
|
-
case
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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)
|
|
352
362
|
|
|
353
|
-
|
|
354
|
-
|
|
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}")
|
|
355
377
|
|
|
356
378
|
if (len(req_multi_items) == MULTI_INFO_REQ_MAX) or \
|
|
357
379
|
(len(req_multi_items) > 0 and idx == idx_last):
|
|
@@ -421,6 +443,36 @@ class XcomApiBase:
|
|
|
421
443
|
time.sleep(1)
|
|
422
444
|
burst_start = datetime.now()
|
|
423
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
|
+
|
|
424
476
|
# Return all reponse items as one XcomValues object
|
|
425
477
|
return XcomValues(result_items)
|
|
426
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,9 +190,13 @@ 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
|
-
VIRTUAL = 0x0000
|
|
193
200
|
INFO = 0x0001
|
|
194
201
|
PARAMETER = 0x0002
|
|
195
202
|
|
|
@@ -208,7 +215,6 @@ class ScomObjType:
|
|
|
208
215
|
def __repr__(self):
|
|
209
216
|
return self.name
|
|
210
217
|
|
|
211
|
-
@staticmethod
|
|
212
218
|
@staticmethod
|
|
213
219
|
def from_str(s: str, default: int|None = None):
|
|
214
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
|
|
|
@@ -82,9 +88,6 @@ class XcomDatapoint:
|
|
|
82
88
|
|
|
83
89
|
@property
|
|
84
90
|
def category(self) -> XcomCategory:
|
|
85
|
-
if self.nr >= 99000:
|
|
86
|
-
return XcomCategory.VIRTUAL
|
|
87
|
-
|
|
88
91
|
if self.level in [XcomLevel.INFO]:
|
|
89
92
|
return XcomCategory.INFO
|
|
90
93
|
|
|
@@ -94,6 +97,13 @@ class XcomDatapoint:
|
|
|
94
97
|
_LOGGER.debug(f"Unknown category for datapoint {self.nr} with level {self.level} and format {self.format}")
|
|
95
98
|
return XcomCategory.INFO
|
|
96
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
|
+
|
|
97
107
|
def enum_value(self, key):
|
|
98
108
|
if self.format not in [XcomFormat.LONG_ENUM, XcomFormat.SHORT_ENUM]:
|
|
99
109
|
return None
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
[
|
|
2
2
|
{ "_rem": "Extra 'virtual' Xcom datapoints"},
|
|
3
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
|
+
|
|
4
7
|
{ "_rem": "Column descriptions:"},
|
|
5
8
|
{ "_rem": " fam = Device Family"},
|
|
6
9
|
{ "_rem": " lvl = User level required to access the param or info"},
|
|
@@ -26,14 +29,14 @@
|
|
|
26
29
|
{ "_rem": "###"},
|
|
27
30
|
{ "_rem": "### Xcom specific params."},
|
|
28
31
|
{ "_rem": "###"},
|
|
29
|
-
{ "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99000, "name": "Is message pending", "short": "", "urcc": "", "unit": "",
|
|
30
|
-
{ "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99001, "name": "Was RCC reseted", "short": "", "urcc": "", "unit": "",
|
|
31
|
-
{ "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99002, "name": "Is sd-card present", "short": "", "urcc": "", "unit": "",
|
|
32
|
-
{ "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99003, "name": "Is sd-card full", "short": "", "urcc": "", "unit": "",
|
|
33
|
-
{ "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99004, "name": "Is new datalogger file present", "short": "", "urcc": "", "unit": "",
|
|
34
|
-
{ "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99020, "name": "Latest message", "short": "", "urcc": "", "unit": "",
|
|
35
|
-
{ "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99021, "name": "Latest message time", "short": "", "urcc": "", "unit": "", "fmt": "
|
|
36
|
-
{ "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99022, "name": "Latest message source", "short": "", "urcc": "", "unit": "",
|
|
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": "STRING" },
|
|
38
|
+
{ "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99021, "name": "Latest message time", "short": "", "urcc": "", "unit": "ts", "fmt": "INT32" },
|
|
39
|
+
{ "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99022, "name": "Latest message source", "short": "", "urcc": "", "unit": "addr", "fmt": "INT32" },
|
|
37
40
|
|
|
38
41
|
{ "_rem": "###"},
|
|
39
42
|
{ "_rem": "### End of file"},
|
|
@@ -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
|
|
|
@@ -139,10 +139,10 @@ class XcomDeviceFamilies:
|
|
|
139
139
|
)
|
|
140
140
|
XCOM = XcomDeviceFamily(
|
|
141
141
|
"xcom", "xcom", # virtual device to expose additional values in a uniform way
|
|
142
|
-
"Xcom",
|
|
142
|
+
"Xcom-LAN/232i",
|
|
143
143
|
990, # addr multicast to all devices (write only)
|
|
144
144
|
990, 990, # addr devices, start to end
|
|
145
|
-
|
|
145
|
+
98000, 98999, # nr for params, start to end
|
|
146
146
|
99000, 99999, # nr for infos, start to end
|
|
147
147
|
99000, # nr for discovery
|
|
148
148
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pystuderxcom
|
|
3
|
-
Version: 3.4.
|
|
3
|
+
Version: 3.4.3
|
|
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
|