pystuderxcom 3.4.1__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.
Files changed (51) hide show
  1. {pystuderxcom-3.4.1/src/pystuderxcom.egg-info → pystuderxcom-3.4.2}/PKG-INFO +1 -1
  2. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/pyproject.toml +1 -1
  3. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom/__init__.py +1 -1
  4. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom/api_base_async.py +74 -22
  5. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom/api_base_sync.py +75 -23
  6. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom/const.py +8 -2
  7. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom/datapoints.py +13 -3
  8. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom/datapoints_xcom.json +3 -0
  9. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom/discover_async.py +5 -1
  10. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom/discover_sync.py +5 -1
  11. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom/families.py +2 -2
  12. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2/src/pystuderxcom.egg-info}/PKG-INFO +1 -1
  13. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/tests/test_api_base_async.py +112 -18
  14. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/tests/test_api_base_sync.py +112 -18
  15. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/tests/test_datapoints_async.py +8 -1
  16. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/tests/test_datapoints_sync.py +8 -1
  17. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/LICENSE +0 -0
  18. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/README.md +0 -0
  19. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/setup.cfg +0 -0
  20. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom/api_serial.py +0 -0
  21. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom/api_tcp.py +0 -0
  22. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom/api_udp.py +0 -0
  23. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom/data.py +0 -0
  24. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom/datapoints_120v.json +0 -0
  25. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom/datapoints_240v.json +0 -0
  26. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom/factory_async.py +0 -0
  27. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom/factory_sync.py +0 -0
  28. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom/messages.py +0 -0
  29. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom/messages_en.json +0 -0
  30. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom/protocol.py +0 -0
  31. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom/values.py +0 -0
  32. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom.egg-info/SOURCES.txt +0 -0
  33. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom.egg-info/dependency_links.txt +0 -0
  34. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom.egg-info/requires.txt +0 -0
  35. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/src/pystuderxcom.egg-info/top_level.txt +0 -0
  36. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/tests/test_api_serial_async.py +0 -0
  37. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/tests/test_api_serial_sync.py +0 -0
  38. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/tests/test_api_tcp_async.py +0 -0
  39. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/tests/test_api_tcp_sync.py +0 -0
  40. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/tests/test_api_udp_async.py +0 -0
  41. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/tests/test_api_udp_sync.py +0 -0
  42. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/tests/test_const.py +0 -0
  43. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/tests/test_data.py +0 -0
  44. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/tests/test_discover_async.py +0 -0
  45. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/tests/test_discover_sync.py +0 -0
  46. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/tests/test_families.py +0 -0
  47. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/tests/test_messageset_async.py +0 -0
  48. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/tests/test_messageset_sync.py +0 -0
  49. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/tests/test_package_async.py +0 -0
  50. {pystuderxcom-3.4.1 → pystuderxcom-3.4.2}/tests/test_package_sync.py +0 -0
  51. {pystuderxcom-3.4.1 → 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.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pystuderxcom"
3
- version = "3.4.1"
3
+ version = "3.4.2"
4
4
  authors = [
5
5
  { name="Anko Hanse", email="anko_hanse@hotmail.com" },
6
6
  ]
@@ -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 and params in one call.
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.category:
332
- case XcomCategory.INFO:
333
- if item.aggregation_type is not None and item.aggregation_type in range(XcomAggregationType.DEVICE1, XcomAggregationType.DEVICE15+1):
334
- # Can be combined with other infos in a request_values call
335
- req_multi_items.append(item)
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 XcomCategory.PARAMETER:
345
- if item.address is not None:
346
- # Needs to be done via an individual request_value call
347
- req_singles.append(item)
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
- else:
350
- raise XcomParamException(f"Invalid XcomValuesItem passed to request_values; violated by code='{item.code}', address={item.address}, aggregation_type={item.aggregation_type}")
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 and params in one call.
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.category:
336
- case XcomCategory.INFO:
337
- if item.aggregation_type is not None and item.aggregation_type in range(XcomAggregationType.DEVICE1, XcomAggregationType.DEVICE15+1):
338
- # Can be combined with other infos in a request_values call
339
- req_multi_items.append(item)
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 XcomCategory.PARAMETER:
349
- if item.address is not None:
350
- # Needs to be done via an individual request_value call
351
- req_singles.append(item)
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
- else:
354
- raise XcomParamException(f"Invalid XcomValuesItem passed to request_values; violated by code='{item.code}', address={item.address}, aggregation_type={item.aggregation_type}")
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"},
@@ -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
- value = await self._api.request_value(param, device_addr, verbose=verbose)
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
- value = self._api.request_value(param, device_addr, verbose=verbose)
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
- 0, 0, # nr for params, start to end
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.1
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
@@ -9,8 +9,9 @@ from pystuderxcom import AsyncXcomFactory, XcomFactory
9
9
  from pystuderxcom import XcomApiTimeoutException, XcomApiResponseIsError, XcomParamException
10
10
  from pystuderxcom import XcomDataset, XcomData, XcomPackage
11
11
  from pystuderxcom import XcomValues, XcomValuesItem
12
- from pystuderxcom import XcomVoltage, XcomFormat, XcomAggregationType, ScomServiceId, ScomServiceFlag, ScomFrameFlag, ScomObjType, ScomObjId, ScomQspId, ScomAddress, ScomErrorCode
12
+ from pystuderxcom import XcomVoltage, XcomFormat, XcomAggregationType, XcomTarget, ScomServiceId, ScomServiceFlag, ScomFrameFlag, ScomObjType, ScomObjId, ScomQspId, ScomAddress, ScomErrorCode
13
13
  from pystuderxcom import XcomDataMessageRsp
14
+
14
15
  from . import AsyncTestApi, TestApi
15
16
  from . import AsyncTaskHelper, TaskHelper
16
17
 
@@ -63,6 +64,7 @@ async def data_message():
63
64
  @pytest.mark.parametrize(
64
65
  "name, test_nr, test_dest, rsp_frm_flags, rsp_data, exp_value, exp_except",
65
66
  [
67
+ ("request virtual err", 3000, 100, 0x00, None, None, XcomParamException),
66
68
  ("request virtual false", 99000, 990, 0x00, None, False, None),
67
69
  ("request virtual is_message_pending", 99000, 990, ScomFrameFlag.IS_MESSAGE_PENDING, None, True, None),
68
70
  ("request virtual was_rcc_reseted", 99001, 990, ScomFrameFlag.WAS_RCC_RESETED, None, True, None),
@@ -99,7 +101,6 @@ async def test_request_virtual(name, test_nr, test_dest, rsp_frm_flags, rsp_data
99
101
 
100
102
  if exp_except == None:
101
103
  value = await api.request_virtual(param, test_dest, retries=1, timeout=5)
102
-
103
104
  assert value == exp_value
104
105
  else:
105
106
  with pytest.raises(exp_except):
@@ -162,6 +163,7 @@ async def test_request_guid(name, exp_dst_addr, exp_svc_id, exp_obj_type, exp_ob
162
163
  ("request info timeout", 3000, 100, 100, ScomServiceId.READ, ScomObjType.INFO, 3000, ScomQspId.VALUE, 0x00, XcomData.pack(1234.0, XcomFormat.FLOAT), None, XcomApiTimeoutException),
163
164
  ("request param ok", 1107, 100, 100, ScomServiceId.READ, ScomObjType.PARAMETER, 1107, ScomQspId.UNSAVED_VALUE, 0x02, XcomData.pack(1234.0, XcomFormat.FLOAT), 1234.0, None),
164
165
  ("request param vo", 5012, 501, 501, ScomServiceId.READ, ScomObjType.PARAMETER, 5012, ScomQspId.UNSAVED_VALUE, 0x02, XcomData.pack(32, XcomFormat.INT32), 32, None),
166
+ ("request virtual err", 99003, 990, 990, ScomServiceId.READ, ScomObjType.INFO, 3000, ScomQspId.VALUE, 0x02, XcomData.pack(1234.0, XcomFormat.FLOAT), None, XcomParamException),
165
167
  ]
166
168
  )
167
169
  async def test_request_value(name, test_nr, test_dest, exp_dst_addr, exp_svc_id, exp_obj_type, exp_obj_id, exp_prop_id, rsp_flags, rsp_data, exp_value, exp_except, request):
@@ -173,6 +175,7 @@ async def test_request_value(name, test_nr, test_dest, exp_dst_addr, exp_svc_id,
173
175
  """Helper to turn a request into a response"""
174
176
  api.response_package = copy.deepcopy(api.request_package)
175
177
 
178
+ api.response_package.header.frame_flags = ScomFrameFlag.IS_MESSAGE_PENDING
176
179
  api.response_package.frame_data.service_flags = rsp_flags
177
180
  api.response_package.frame_data.service_data.property_data = rsp_data
178
181
  api.response_package.header.data_length = len(api.response_package.frame_data)
@@ -281,9 +284,12 @@ async def data_infos_dev(dataset):
281
284
  XcomValuesItem(info_3023, aggregation_type=XcomAggregationType.DEVICE1, value=78.9),
282
285
  ]
283
286
  )
284
- rsp_single_val = None
285
-
286
- yield req_data, rsp_multi, rsp_single_val
287
+ rsp_single = XcomValues(
288
+ flags = 0x00,
289
+ datetime = 0,
290
+ items=[]
291
+ )
292
+ yield req_data, rsp_multi, rsp_single
287
293
 
288
294
  @pytest_asyncio.fixture
289
295
  async def data_infos_aggr(dataset):
@@ -305,9 +311,12 @@ async def data_infos_aggr(dataset):
305
311
  XcomValuesItem(info_3023, aggregation_type=XcomAggregationType.SUM, value=78.9),
306
312
  ]
307
313
  )
308
- rsp_single_val = None
309
-
310
- yield req_data, rsp_multi, rsp_single_val
314
+ rsp_single = XcomValues(
315
+ flags = 0x00,
316
+ datetime = 0,
317
+ items=[]
318
+ )
319
+ yield req_data, rsp_multi, rsp_single
311
320
 
312
321
  @pytest_asyncio.fixture
313
322
  async def data_infos_params_dev(dataset):
@@ -328,9 +337,14 @@ async def data_infos_params_dev(dataset):
328
337
  XcomValuesItem(info_3022, aggregation_type=XcomAggregationType.DEVICE1, value=45.6),
329
338
  ]
330
339
  )
331
- rsp_single_val = 1234.0
332
-
333
- yield req_data, rsp_multi, rsp_single_val
340
+ rsp_single = XcomValues(
341
+ flags = 0x00,
342
+ datetime = 0,
343
+ items=[
344
+ XcomValuesItem(param_1107, aggregation_type=XcomAggregationType.DEVICE1, value=1234.0),
345
+ ]
346
+ )
347
+ yield req_data, rsp_multi, rsp_single
334
348
 
335
349
 
336
350
  @pytest_asyncio.fixture
@@ -352,13 +366,82 @@ async def data_infos_params_aggr(dataset):
352
366
  XcomValuesItem(info_3022, aggregation_type=XcomAggregationType.AVERAGE, value=45.6),
353
367
  ]
354
368
  )
355
- rsp_single_val = 1234.0
369
+ rsp_single = XcomValues(
370
+ flags = 0x00,
371
+ datetime = 0,
372
+ items=[
373
+ XcomValuesItem(param_1107, aggregation_type=XcomAggregationType.DEVICE1, value=1234.0),
374
+ ]
375
+ )
376
+ yield req_data, rsp_multi, rsp_single
377
+
378
+
379
+ @pytest_asyncio.fixture
380
+ async def data_infos_virt_dev(dataset):
381
+ info_3021 = dataset.get_by_nr(3021)
382
+ info_3022 = dataset.get_by_nr(3022)
383
+ param_1107 = dataset.get_by_nr(1107)
384
+ info_99003 = dataset.get_by_nr(99003)
385
+
386
+ req_data = XcomValues([
387
+ XcomValuesItem(info_3021, code="XT1"),
388
+ XcomValuesItem(info_3022, aggregation_type=XcomAggregationType.DEVICE1),
389
+ XcomValuesItem(param_1107, address=101),
390
+ XcomValuesItem(info_99003, code="XCOM"), # Make sure to have another single request prior to virtual
391
+ ])
392
+ rsp_multi = XcomValues(
393
+ flags = 0x00,
394
+ datetime = 0,
395
+ items=[
396
+ XcomValuesItem(info_3021, aggregation_type=XcomAggregationType.MASTER, value=12.3),
397
+ XcomValuesItem(info_3022, aggregation_type=XcomAggregationType.DEVICE1, value=45.6),
398
+ ]
399
+ )
400
+ rsp_single = XcomValues(
401
+ flags = 0x00,
402
+ datetime = 0,
403
+ items=[
404
+ XcomValuesItem(param_1107, aggregation_type=XcomAggregationType.DEVICE1, value=1234.0),
405
+ XcomValuesItem(info_99003, aggregation_type=XcomAggregationType.DEVICE1, value=True),
406
+ ]
407
+ )
408
+ yield req_data, rsp_multi, rsp_single
409
+
356
410
 
357
- yield req_data, rsp_multi, rsp_single_val
411
+ @pytest_asyncio.fixture
412
+ async def data_infos_virt_aggr(dataset):
413
+ info_3021 = dataset.get_by_nr(3021)
414
+ info_3022 = dataset.get_by_nr(3022)
415
+ param_1107 = dataset.get_by_nr(1107)
416
+ info_99003 = dataset.get_by_nr(99003)
417
+
418
+ req_data = XcomValues([
419
+ XcomValuesItem(info_3021, aggregation_type=XcomAggregationType.MASTER),
420
+ XcomValuesItem(info_3022, aggregation_type=XcomAggregationType.AVERAGE),
421
+ XcomValuesItem(param_1107, address=101),
422
+ XcomValuesItem(info_99003, code="XCOM"), # Make sure to have another single request prior to virtual
423
+ ])
424
+ rsp_multi = XcomValues(
425
+ flags = 0x00,
426
+ datetime = 0,
427
+ items=[
428
+ XcomValuesItem(info_3021, aggregation_type=XcomAggregationType.MASTER, value=12.3),
429
+ XcomValuesItem(info_3022, aggregation_type=XcomAggregationType.AVERAGE, value=45.6),
430
+ ]
431
+ )
432
+ rsp_single = XcomValues(
433
+ flags = 0x00,
434
+ datetime = 0,
435
+ items=[
436
+ XcomValuesItem(param_1107, aggregation_type=XcomAggregationType.DEVICE1, value=1234.0),
437
+ XcomValuesItem(info_99003, aggregation_type=XcomAggregationType.DEVICE1, value=True),
438
+ ]
439
+ )
440
+ yield req_data, rsp_multi, rsp_single
358
441
 
359
442
 
360
443
  @pytest.mark.asyncio
361
- @pytest.mark.usefixtures("dataset", "data_infos_dev", "data_infos_aggr", "data_infos_params_dev", "data_infos_params_aggr")
444
+ @pytest.mark.usefixtures("dataset", "data_infos_dev", "data_infos_aggr", "data_infos_params_dev", "data_infos_params_aggr", "data_infos_virt_dev", "data_infos_virt_aggr")
362
445
  @pytest.mark.parametrize(
363
446
  "name, values_fixture, run_receive, exp_src_addr, exp_dst_addr, exp_svc_id, exp_obj_type, exp_obj_id, exp_prop_id, rsp_flags, exp_except",
364
447
  [
@@ -370,6 +453,8 @@ async def data_infos_params_aggr(dataset):
370
453
  ("request infos aggr timeout", "data_infos_aggr", False, ScomAddress.SOURCE, 501, ScomServiceId.READ, ScomObjType.MULTI_INFO, ScomObjId.MULTI_INFO, ScomQspId.MULTI_INFO, 0x02, XcomApiTimeoutException),
371
454
  ("request infos params err", "data_infos_params_dev", False, ScomAddress.SOURCE, 501, ScomServiceId.READ, ScomObjType.MULTI_INFO, ScomObjId.MULTI_INFO, ScomQspId.MULTI_INFO, 0x03, XcomParamException),
372
455
  ("request infos params aggr", "data_infos_params_aggr", False, ScomAddress.SOURCE, 501, ScomServiceId.READ, ScomObjType.MULTI_INFO, ScomObjId.MULTI_INFO, ScomQspId.MULTI_INFO, 0x03, XcomParamException),
456
+ ("request infos virt err", "data_infos_virt_dev", False, ScomAddress.SOURCE, 501, ScomServiceId.READ, ScomObjType.MULTI_INFO, ScomObjId.MULTI_INFO, ScomQspId.MULTI_INFO, 0x03, XcomParamException),
457
+ ("request infos virt aggr", "data_infos_virt_aggr", False, ScomAddress.SOURCE, 501, ScomServiceId.READ, ScomObjType.MULTI_INFO, ScomObjId.MULTI_INFO, ScomQspId.MULTI_INFO, 0x02, XcomParamException),
373
458
  ]
374
459
  )
375
460
  async def test_request_infos(name, values_fixture, run_receive, exp_src_addr, exp_dst_addr, exp_svc_id, exp_obj_type, exp_obj_id, exp_prop_id, rsp_flags, exp_except, request):
@@ -383,6 +468,7 @@ async def test_request_infos(name, values_fixture, run_receive, exp_src_addr, ex
383
468
  else:
384
469
  api.response_package = copy.deepcopy(api.request_package)
385
470
 
471
+ api.response_package.header.frame_flags = 0x1F
386
472
  api.response_package.frame_data.service_flags = rsp_flags
387
473
  if rsp_flags & 0x01:
388
474
  api.response_package.frame_data.service_data.property_data = XcomData.pack(ScomErrorCode.READ_PROPERTY_FAILED, XcomFormat.ERROR)
@@ -435,7 +521,7 @@ async def test_request_infos(name, values_fixture, run_receive, exp_src_addr, ex
435
521
 
436
522
 
437
523
  @pytest.mark.asyncio
438
- @pytest.mark.usefixtures("dataset", "data_infos_dev", "data_infos_aggr", "data_infos_params_dev", "data_infos_params_aggr")
524
+ @pytest.mark.usefixtures("dataset", "data_infos_dev", "data_infos_aggr", "data_infos_params_dev", "data_infos_params_aggr", "data_infos_virt_dev", "data_infos_virt_aggr")
439
525
  @pytest.mark.parametrize(
440
526
  "name, values_fixture, run_receive, rsp_flags, exp_value, exp_error, exp_except",
441
527
  [
@@ -445,11 +531,13 @@ async def test_request_infos(name, values_fixture, run_receive, exp_src_addr, ex
445
531
  ("request values infos aggr", "data_infos_aggr", False, 0x02, False, False, XcomParamException),
446
532
  ("request values params ok", "data_infos_params_dev", True, 0x02, True, False, None),
447
533
  ("request values params aggr", "data_infos_params_aggr", False, 0x02, False, False, XcomParamException),
534
+ ("request values virt ok", "data_infos_virt_dev", True, 0x02, True, False, None),
535
+ ("request values virt aggr", "data_infos_virt_aggr", False, 0x02, False, False, XcomParamException),
448
536
  ]
449
537
  )
450
538
  async def test_request_values(name, values_fixture, run_receive, rsp_flags, exp_value, exp_error, exp_except, request):
451
539
 
452
- req_data, exp_rsp_multi, exp_rsp_single_val = request.getfixturevalue(values_fixture)
540
+ req_data, exp_rsp_multi, exp_rsp_single = request.getfixturevalue(values_fixture)
453
541
 
454
542
  async def on_receive(api: AsyncTestApi):
455
543
  """Helper to turn a request into a response"""
@@ -457,6 +545,7 @@ async def test_request_values(name, values_fixture, run_receive, rsp_flags, exp_
457
545
  api.response_package = None
458
546
  else:
459
547
  api.response_package = copy.deepcopy(api.request_package)
548
+ api.response_package.header.frame_flags = 0x1F
460
549
  api.response_package.frame_data.service_flags = rsp_flags
461
550
 
462
551
  if rsp_flags & 0x01:
@@ -466,7 +555,8 @@ async def test_request_values(name, values_fixture, run_receive, rsp_flags, exp_
466
555
  api.response_package.frame_data.service_data.property_data = exp_rsp_multi.pack_response()
467
556
 
468
557
  else:
469
- api.response_package.frame_data.service_data.property_data = XcomData.pack(exp_rsp_single_val, XcomFormat.FLOAT)
558
+ item = next( (i for i in exp_rsp_single.items if i.datapoint.nr == api.request_package.frame_data.service_data.object_id), None)
559
+ api.response_package.frame_data.service_data.property_data = XcomData.pack(item.value, XcomFormat.FLOAT)
470
560
 
471
561
  api.response_package.header.data_length = len(api.response_package.frame_data)
472
562
 
@@ -494,7 +584,11 @@ async def test_request_values(name, values_fixture, run_receive, rsp_flags, exp_
494
584
  if exp_item is not None:
495
585
  exp_val = exp_item.value
496
586
  else:
497
- exp_val = exp_rsp_single_val
587
+ exp_item = next((i for i in exp_rsp_single.items if i.datapoint.nr==item.datapoint.nr and i.aggregation_type==item.aggregation_type), None)
588
+ if exp_item is not None:
589
+ exp_val = exp_item.value
590
+ else:
591
+ exp_val = None
498
592
 
499
593
  match item.datapoint.format:
500
594
  case XcomFormat.FLOAT:
@@ -11,8 +11,9 @@ from pystuderxcom import AsyncXcomFactory, XcomFactory
11
11
  from pystuderxcom import XcomApiTimeoutException, XcomApiResponseIsError, XcomParamException
12
12
  from pystuderxcom import XcomDataset, XcomData, XcomPackage
13
13
  from pystuderxcom import XcomValues, XcomValuesItem
14
- from pystuderxcom import XcomVoltage, XcomFormat, XcomAggregationType, ScomServiceId, ScomServiceFlag, ScomFrameFlag, ScomObjType, ScomObjId, ScomQspId, ScomAddress, ScomErrorCode
14
+ from pystuderxcom import XcomVoltage, XcomFormat, XcomAggregationType, XcomTarget, ScomServiceId, ScomServiceFlag, ScomFrameFlag, ScomObjType, ScomObjId, ScomQspId, ScomAddress, ScomErrorCode
15
15
  from pystuderxcom import XcomDataMessageRsp
16
+
16
17
  from . import AsyncTestApi, TestApi
17
18
  from . import AsyncTaskHelper, TaskHelper
18
19
 
@@ -65,6 +66,7 @@ def data_message():
65
66
  @pytest.mark.parametrize(
66
67
  "name, test_nr, test_dest, rsp_frm_flags, rsp_data, exp_value, exp_except",
67
68
  [
69
+ ("request virtual err", 3000, 100, 0x00, None, None, XcomParamException),
68
70
  ("request virtual false", 99000, 990, 0x00, None, False, None),
69
71
  ("request virtual is_message_pending", 99000, 990, ScomFrameFlag.IS_MESSAGE_PENDING, None, True, None),
70
72
  ("request virtual was_rcc_reseted", 99001, 990, ScomFrameFlag.WAS_RCC_RESETED, None, True, None),
@@ -101,7 +103,6 @@ def test_request_virtual(name, test_nr, test_dest, rsp_frm_flags, rsp_data,exp_v
101
103
 
102
104
  if exp_except == None:
103
105
  value = api.request_virtual(param, test_dest, retries=1, timeout=5)
104
-
105
106
  assert value == exp_value
106
107
  else:
107
108
  with pytest.raises(exp_except):
@@ -164,6 +165,7 @@ def test_request_guid(name, exp_dst_addr, exp_svc_id, exp_obj_type, exp_obj_id,
164
165
  ("request info timeout", 3000, 100, 100, ScomServiceId.READ, ScomObjType.INFO, 3000, ScomQspId.VALUE, 0x00, XcomData.pack(1234.0, XcomFormat.FLOAT), None, XcomApiTimeoutException),
165
166
  ("request param ok", 1107, 100, 100, ScomServiceId.READ, ScomObjType.PARAMETER, 1107, ScomQspId.UNSAVED_VALUE, 0x02, XcomData.pack(1234.0, XcomFormat.FLOAT), 1234.0, None),
166
167
  ("request param vo", 5012, 501, 501, ScomServiceId.READ, ScomObjType.PARAMETER, 5012, ScomQspId.UNSAVED_VALUE, 0x02, XcomData.pack(32, XcomFormat.INT32), 32, None),
168
+ ("request virtual err", 99003, 990, 990, ScomServiceId.READ, ScomObjType.INFO, 3000, ScomQspId.VALUE, 0x02, XcomData.pack(1234.0, XcomFormat.FLOAT), None, XcomParamException),
167
169
  ]
168
170
  )
169
171
  def test_request_value(name, test_nr, test_dest, exp_dst_addr, exp_svc_id, exp_obj_type, exp_obj_id, exp_prop_id, rsp_flags, rsp_data, exp_value, exp_except, request):
@@ -175,6 +177,7 @@ def test_request_value(name, test_nr, test_dest, exp_dst_addr, exp_svc_id, exp_o
175
177
  """Helper to turn a request into a response"""
176
178
  api.response_package = copy.deepcopy(api.request_package)
177
179
 
180
+ api.response_package.header.frame_flags = ScomFrameFlag.IS_MESSAGE_PENDING
178
181
  api.response_package.frame_data.service_flags = rsp_flags
179
182
  api.response_package.frame_data.service_data.property_data = rsp_data
180
183
  api.response_package.header.data_length = len(api.response_package.frame_data)
@@ -283,9 +286,12 @@ def data_infos_dev(dataset):
283
286
  XcomValuesItem(info_3023, aggregation_type=XcomAggregationType.DEVICE1, value=78.9),
284
287
  ]
285
288
  )
286
- rsp_single_val = None
287
-
288
- yield req_data, rsp_multi, rsp_single_val
289
+ rsp_single = XcomValues(
290
+ flags = 0x00,
291
+ datetime = 0,
292
+ items=[]
293
+ )
294
+ yield req_data, rsp_multi, rsp_single
289
295
 
290
296
  @pytest.fixture
291
297
  def data_infos_aggr(dataset):
@@ -307,9 +313,12 @@ def data_infos_aggr(dataset):
307
313
  XcomValuesItem(info_3023, aggregation_type=XcomAggregationType.SUM, value=78.9),
308
314
  ]
309
315
  )
310
- rsp_single_val = None
311
-
312
- yield req_data, rsp_multi, rsp_single_val
316
+ rsp_single = XcomValues(
317
+ flags = 0x00,
318
+ datetime = 0,
319
+ items=[]
320
+ )
321
+ yield req_data, rsp_multi, rsp_single
313
322
 
314
323
  @pytest.fixture
315
324
  def data_infos_params_dev(dataset):
@@ -330,9 +339,14 @@ def data_infos_params_dev(dataset):
330
339
  XcomValuesItem(info_3022, aggregation_type=XcomAggregationType.DEVICE1, value=45.6),
331
340
  ]
332
341
  )
333
- rsp_single_val = 1234.0
334
-
335
- yield req_data, rsp_multi, rsp_single_val
342
+ rsp_single = XcomValues(
343
+ flags = 0x00,
344
+ datetime = 0,
345
+ items=[
346
+ XcomValuesItem(param_1107, aggregation_type=XcomAggregationType.DEVICE1, value=1234.0),
347
+ ]
348
+ )
349
+ yield req_data, rsp_multi, rsp_single
336
350
 
337
351
 
338
352
  @pytest.fixture
@@ -354,13 +368,82 @@ def data_infos_params_aggr(dataset):
354
368
  XcomValuesItem(info_3022, aggregation_type=XcomAggregationType.AVERAGE, value=45.6),
355
369
  ]
356
370
  )
357
- rsp_single_val = 1234.0
371
+ rsp_single = XcomValues(
372
+ flags = 0x00,
373
+ datetime = 0,
374
+ items=[
375
+ XcomValuesItem(param_1107, aggregation_type=XcomAggregationType.DEVICE1, value=1234.0),
376
+ ]
377
+ )
378
+ yield req_data, rsp_multi, rsp_single
379
+
380
+
381
+ @pytest.fixture
382
+ def data_infos_virt_dev(dataset):
383
+ info_3021 = dataset.get_by_nr(3021)
384
+ info_3022 = dataset.get_by_nr(3022)
385
+ param_1107 = dataset.get_by_nr(1107)
386
+ info_99003 = dataset.get_by_nr(99003)
387
+
388
+ req_data = XcomValues([
389
+ XcomValuesItem(info_3021, code="XT1"),
390
+ XcomValuesItem(info_3022, aggregation_type=XcomAggregationType.DEVICE1),
391
+ XcomValuesItem(param_1107, address=101),
392
+ XcomValuesItem(info_99003, code="XCOM"), # Make sure to have another single request prior to virtual
393
+ ])
394
+ rsp_multi = XcomValues(
395
+ flags = 0x00,
396
+ datetime = 0,
397
+ items=[
398
+ XcomValuesItem(info_3021, aggregation_type=XcomAggregationType.MASTER, value=12.3),
399
+ XcomValuesItem(info_3022, aggregation_type=XcomAggregationType.DEVICE1, value=45.6),
400
+ ]
401
+ )
402
+ rsp_single = XcomValues(
403
+ flags = 0x00,
404
+ datetime = 0,
405
+ items=[
406
+ XcomValuesItem(param_1107, aggregation_type=XcomAggregationType.DEVICE1, value=1234.0),
407
+ XcomValuesItem(info_99003, aggregation_type=XcomAggregationType.DEVICE1, value=True),
408
+ ]
409
+ )
410
+ yield req_data, rsp_multi, rsp_single
411
+
358
412
 
359
- yield req_data, rsp_multi, rsp_single_val
413
+ @pytest.fixture
414
+ def data_infos_virt_aggr(dataset):
415
+ info_3021 = dataset.get_by_nr(3021)
416
+ info_3022 = dataset.get_by_nr(3022)
417
+ param_1107 = dataset.get_by_nr(1107)
418
+ info_99003 = dataset.get_by_nr(99003)
419
+
420
+ req_data = XcomValues([
421
+ XcomValuesItem(info_3021, aggregation_type=XcomAggregationType.MASTER),
422
+ XcomValuesItem(info_3022, aggregation_type=XcomAggregationType.AVERAGE),
423
+ XcomValuesItem(param_1107, address=101),
424
+ XcomValuesItem(info_99003, code="XCOM"), # Make sure to have another single request prior to virtual
425
+ ])
426
+ rsp_multi = XcomValues(
427
+ flags = 0x00,
428
+ datetime = 0,
429
+ items=[
430
+ XcomValuesItem(info_3021, aggregation_type=XcomAggregationType.MASTER, value=12.3),
431
+ XcomValuesItem(info_3022, aggregation_type=XcomAggregationType.AVERAGE, value=45.6),
432
+ ]
433
+ )
434
+ rsp_single = XcomValues(
435
+ flags = 0x00,
436
+ datetime = 0,
437
+ items=[
438
+ XcomValuesItem(param_1107, aggregation_type=XcomAggregationType.DEVICE1, value=1234.0),
439
+ XcomValuesItem(info_99003, aggregation_type=XcomAggregationType.DEVICE1, value=True),
440
+ ]
441
+ )
442
+ yield req_data, rsp_multi, rsp_single
360
443
 
361
444
 
362
445
  @pytest.mark.asyncio
363
- @pytest.mark.usefixtures("dataset", "data_infos_dev", "data_infos_aggr", "data_infos_params_dev", "data_infos_params_aggr")
446
+ @pytest.mark.usefixtures("dataset", "data_infos_dev", "data_infos_aggr", "data_infos_params_dev", "data_infos_params_aggr", "data_infos_virt_dev", "data_infos_virt_aggr")
364
447
  @pytest.mark.parametrize(
365
448
  "name, values_fixture, run_receive, exp_src_addr, exp_dst_addr, exp_svc_id, exp_obj_type, exp_obj_id, exp_prop_id, rsp_flags, exp_except",
366
449
  [
@@ -372,6 +455,8 @@ def data_infos_params_aggr(dataset):
372
455
  ("request infos aggr timeout", "data_infos_aggr", False, ScomAddress.SOURCE, 501, ScomServiceId.READ, ScomObjType.MULTI_INFO, ScomObjId.MULTI_INFO, ScomQspId.MULTI_INFO, 0x02, XcomApiTimeoutException),
373
456
  ("request infos params err", "data_infos_params_dev", False, ScomAddress.SOURCE, 501, ScomServiceId.READ, ScomObjType.MULTI_INFO, ScomObjId.MULTI_INFO, ScomQspId.MULTI_INFO, 0x03, XcomParamException),
374
457
  ("request infos params aggr", "data_infos_params_aggr", False, ScomAddress.SOURCE, 501, ScomServiceId.READ, ScomObjType.MULTI_INFO, ScomObjId.MULTI_INFO, ScomQspId.MULTI_INFO, 0x03, XcomParamException),
458
+ ("request infos virt err", "data_infos_virt_dev", False, ScomAddress.SOURCE, 501, ScomServiceId.READ, ScomObjType.MULTI_INFO, ScomObjId.MULTI_INFO, ScomQspId.MULTI_INFO, 0x03, XcomParamException),
459
+ ("request infos virt aggr", "data_infos_virt_aggr", False, ScomAddress.SOURCE, 501, ScomServiceId.READ, ScomObjType.MULTI_INFO, ScomObjId.MULTI_INFO, ScomQspId.MULTI_INFO, 0x02, XcomParamException),
375
460
  ]
376
461
  )
377
462
  def test_request_infos(name, values_fixture, run_receive, exp_src_addr, exp_dst_addr, exp_svc_id, exp_obj_type, exp_obj_id, exp_prop_id, rsp_flags, exp_except, request):
@@ -385,6 +470,7 @@ def test_request_infos(name, values_fixture, run_receive, exp_src_addr, exp_dst_
385
470
  else:
386
471
  api.response_package = copy.deepcopy(api.request_package)
387
472
 
473
+ api.response_package.header.frame_flags = 0x1F
388
474
  api.response_package.frame_data.service_flags = rsp_flags
389
475
  if rsp_flags & 0x01:
390
476
  api.response_package.frame_data.service_data.property_data = XcomData.pack(ScomErrorCode.READ_PROPERTY_FAILED, XcomFormat.ERROR)
@@ -437,7 +523,7 @@ def test_request_infos(name, values_fixture, run_receive, exp_src_addr, exp_dst_
437
523
 
438
524
 
439
525
  @pytest.mark.asyncio
440
- @pytest.mark.usefixtures("dataset", "data_infos_dev", "data_infos_aggr", "data_infos_params_dev", "data_infos_params_aggr")
526
+ @pytest.mark.usefixtures("dataset", "data_infos_dev", "data_infos_aggr", "data_infos_params_dev", "data_infos_params_aggr", "data_infos_virt_dev", "data_infos_virt_aggr")
441
527
  @pytest.mark.parametrize(
442
528
  "name, values_fixture, run_receive, rsp_flags, exp_value, exp_error, exp_except",
443
529
  [
@@ -447,11 +533,13 @@ def test_request_infos(name, values_fixture, run_receive, exp_src_addr, exp_dst_
447
533
  ("request values infos aggr", "data_infos_aggr", False, 0x02, False, False, XcomParamException),
448
534
  ("request values params ok", "data_infos_params_dev", True, 0x02, True, False, None),
449
535
  ("request values params aggr", "data_infos_params_aggr", False, 0x02, False, False, XcomParamException),
536
+ ("request values virt ok", "data_infos_virt_dev", True, 0x02, True, False, None),
537
+ ("request values virt aggr", "data_infos_virt_aggr", False, 0x02, False, False, XcomParamException),
450
538
  ]
451
539
  )
452
540
  def test_request_values(name, values_fixture, run_receive, rsp_flags, exp_value, exp_error, exp_except, request):
453
541
 
454
- req_data, exp_rsp_multi, exp_rsp_single_val = request.getfixturevalue(values_fixture)
542
+ req_data, exp_rsp_multi, exp_rsp_single = request.getfixturevalue(values_fixture)
455
543
 
456
544
  def on_receive(api: TestApi):
457
545
  """Helper to turn a request into a response"""
@@ -459,6 +547,7 @@ def test_request_values(name, values_fixture, run_receive, rsp_flags, exp_value,
459
547
  api.response_package = None
460
548
  else:
461
549
  api.response_package = copy.deepcopy(api.request_package)
550
+ api.response_package.header.frame_flags = 0x1F
462
551
  api.response_package.frame_data.service_flags = rsp_flags
463
552
 
464
553
  if rsp_flags & 0x01:
@@ -468,7 +557,8 @@ def test_request_values(name, values_fixture, run_receive, rsp_flags, exp_value,
468
557
  api.response_package.frame_data.service_data.property_data = exp_rsp_multi.pack_response()
469
558
 
470
559
  else:
471
- api.response_package.frame_data.service_data.property_data = XcomData.pack(exp_rsp_single_val, XcomFormat.FLOAT)
560
+ item = next( (i for i in exp_rsp_single.items if i.datapoint.nr == api.request_package.frame_data.service_data.object_id), None)
561
+ api.response_package.frame_data.service_data.property_data = XcomData.pack(item.value, XcomFormat.FLOAT)
472
562
 
473
563
  api.response_package.header.data_length = len(api.response_package.frame_data)
474
564
 
@@ -496,7 +586,11 @@ def test_request_values(name, values_fixture, run_receive, rsp_flags, exp_value,
496
586
  if exp_item is not None:
497
587
  exp_val = exp_item.value
498
588
  else:
499
- exp_val = exp_rsp_single_val
589
+ exp_item = next((i for i in exp_rsp_single.items if i.datapoint.nr==item.datapoint.nr and i.aggregation_type==item.aggregation_type), None)
590
+ if exp_item is not None:
591
+ exp_val = exp_item.value
592
+ else:
593
+ exp_val = None
500
594
 
501
595
  match item.datapoint.format:
502
596
  case XcomFormat.FLOAT:
@@ -6,6 +6,7 @@ from pystuderxcom import (
6
6
  XcomVoltage,
7
7
  XcomFormat,
8
8
  XcomCategory,
9
+ XcomTarget,
9
10
  XcomDatapointUnknownException,
10
11
  AsyncXcomFactory,
11
12
  XcomFactory,
@@ -39,6 +40,7 @@ async def test_nr():
39
40
  assert param.nr == 1107
40
41
  assert param.format == XcomFormat.FLOAT
41
42
  assert param.category == XcomCategory.PARAMETER
43
+ assert param.target == XcomTarget.STANDARD
42
44
 
43
45
  param = dataset.get_by_nr(1552)
44
46
  assert param.family_id == "xt"
@@ -48,18 +50,21 @@ async def test_nr():
48
50
  assert param.options != None
49
51
  assert type(param.options) is dict
50
52
  assert len(param.options) == 3
53
+ assert param.target == XcomTarget.STANDARD
51
54
 
52
55
  param = dataset.get_by_nr(3000)
53
56
  assert param.family_id == "xt"
54
57
  assert param.nr == 3000
55
58
  assert param.format == XcomFormat.FLOAT
56
59
  assert param.category == XcomCategory.INFO
60
+ assert param.target == XcomTarget.STANDARD
57
61
 
58
62
  param = dataset.get_by_nr(3000, "xt")
59
63
  assert param.family_id == "xt"
60
64
  assert param.nr == 3000
61
65
  assert param.format == XcomFormat.FLOAT
62
66
  assert param.category == XcomCategory.INFO
67
+ assert param.target == XcomTarget.STANDARD
63
68
 
64
69
  param = dataset.get_by_nr(5012, "rcc")
65
70
  assert param.family_id == "rcc"
@@ -68,12 +73,14 @@ async def test_nr():
68
73
  assert param.category == XcomCategory.PARAMETER
69
74
  assert param.options != None
70
75
  assert type(param.options) is dict
76
+ assert param.target == XcomTarget.STANDARD
71
77
 
72
78
  param = dataset.get_by_nr(99000)
73
79
  assert param.family_id == "xcom"
74
80
  assert param.nr == 99000
75
81
  assert param.format == XcomFormat.BOOL
76
- assert param.category == XcomCategory.VIRTUAL
82
+ assert param.category == XcomCategory.INFO
83
+ assert param.target == XcomTarget.VIRTUAL
77
84
 
78
85
  with pytest.raises(XcomDatapointUnknownException):
79
86
  param = dataset.get_by_nr(9999)
@@ -8,6 +8,7 @@ from pystuderxcom import (
8
8
  XcomVoltage,
9
9
  XcomFormat,
10
10
  XcomCategory,
11
+ XcomTarget,
11
12
  XcomDatapointUnknownException,
12
13
  AsyncXcomFactory,
13
14
  XcomFactory,
@@ -41,6 +42,7 @@ def test_nr():
41
42
  assert param.nr == 1107
42
43
  assert param.format == XcomFormat.FLOAT
43
44
  assert param.category == XcomCategory.PARAMETER
45
+ assert param.target == XcomTarget.STANDARD
44
46
 
45
47
  param = dataset.get_by_nr(1552)
46
48
  assert param.family_id == "xt"
@@ -50,18 +52,21 @@ def test_nr():
50
52
  assert param.options != None
51
53
  assert type(param.options) is dict
52
54
  assert len(param.options) == 3
55
+ assert param.target == XcomTarget.STANDARD
53
56
 
54
57
  param = dataset.get_by_nr(3000)
55
58
  assert param.family_id == "xt"
56
59
  assert param.nr == 3000
57
60
  assert param.format == XcomFormat.FLOAT
58
61
  assert param.category == XcomCategory.INFO
62
+ assert param.target == XcomTarget.STANDARD
59
63
 
60
64
  param = dataset.get_by_nr(3000, "xt")
61
65
  assert param.family_id == "xt"
62
66
  assert param.nr == 3000
63
67
  assert param.format == XcomFormat.FLOAT
64
68
  assert param.category == XcomCategory.INFO
69
+ assert param.target == XcomTarget.STANDARD
65
70
 
66
71
  param = dataset.get_by_nr(5012, "rcc")
67
72
  assert param.family_id == "rcc"
@@ -70,12 +75,14 @@ def test_nr():
70
75
  assert param.category == XcomCategory.PARAMETER
71
76
  assert param.options != None
72
77
  assert type(param.options) is dict
78
+ assert param.target == XcomTarget.STANDARD
73
79
 
74
80
  param = dataset.get_by_nr(99000)
75
81
  assert param.family_id == "xcom"
76
82
  assert param.nr == 99000
77
83
  assert param.format == XcomFormat.BOOL
78
- assert param.category == XcomCategory.VIRTUAL
84
+ assert param.category == XcomCategory.INFO
85
+ assert param.target == XcomTarget.VIRTUAL
79
86
 
80
87
  with pytest.raises(XcomDatapointUnknownException):
81
88
  param = dataset.get_by_nr(9999)
File without changes
File without changes
File without changes