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.
Files changed (51) hide show
  1. {pystuderxcom-3.4.1/src/pystuderxcom.egg-info → pystuderxcom-3.4.3}/PKG-INFO +1 -1
  2. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/pyproject.toml +1 -1
  3. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/__init__.py +1 -1
  4. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/api_base_async.py +74 -22
  5. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/api_base_sync.py +75 -23
  6. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/const.py +8 -2
  7. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/datapoints.py +13 -3
  8. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/datapoints_xcom.json +11 -8
  9. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/discover_async.py +5 -1
  10. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/discover_sync.py +5 -1
  11. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/families.py +2 -2
  12. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3/src/pystuderxcom.egg-info}/PKG-INFO +1 -1
  13. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_api_base_async.py +112 -18
  14. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_api_base_sync.py +112 -18
  15. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_datapoints_async.py +8 -1
  16. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_datapoints_sync.py +8 -1
  17. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/LICENSE +0 -0
  18. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/README.md +0 -0
  19. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/setup.cfg +0 -0
  20. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/api_serial.py +0 -0
  21. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/api_tcp.py +0 -0
  22. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/api_udp.py +0 -0
  23. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/data.py +0 -0
  24. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/datapoints_120v.json +0 -0
  25. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/datapoints_240v.json +0 -0
  26. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/factory_async.py +0 -0
  27. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/factory_sync.py +0 -0
  28. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/messages.py +0 -0
  29. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/messages_en.json +0 -0
  30. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/protocol.py +0 -0
  31. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom/values.py +0 -0
  32. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom.egg-info/SOURCES.txt +0 -0
  33. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom.egg-info/dependency_links.txt +0 -0
  34. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom.egg-info/requires.txt +0 -0
  35. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/src/pystuderxcom.egg-info/top_level.txt +0 -0
  36. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_api_serial_async.py +0 -0
  37. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_api_serial_sync.py +0 -0
  38. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_api_tcp_async.py +0 -0
  39. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_api_tcp_sync.py +0 -0
  40. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_api_udp_async.py +0 -0
  41. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_api_udp_sync.py +0 -0
  42. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_const.py +0 -0
  43. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_data.py +0 -0
  44. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_discover_async.py +0 -0
  45. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_discover_sync.py +0 -0
  46. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_families.py +0 -0
  47. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_messageset_async.py +0 -0
  48. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_messageset_sync.py +0 -0
  49. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_package_async.py +0 -0
  50. {pystuderxcom-3.4.1 → pystuderxcom-3.4.3}/tests/test_package_sync.py +0 -0
  51. {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.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pystuderxcom"
3
- version = "3.4.1"
3
+ version = "3.4.3"
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"},
@@ -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": "", "fmt": "BOOL" },
30
- { "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99001, "name": "Was RCC reseted", "short": "", "urcc": "", "unit": "", "fmt": "BOOL" },
31
- { "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99002, "name": "Is sd-card present", "short": "", "urcc": "", "unit": "", "fmt": "BOOL" },
32
- { "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99003, "name": "Is sd-card full", "short": "", "urcc": "", "unit": "", "fmt": "BOOL" },
33
- { "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99004, "name": "Is new datalogger file present", "short": "", "urcc": "", "unit": "", "fmt": "BOOL" },
34
- { "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99020, "name": "Latest message", "short": "", "urcc": "", "unit": "", "fmt": "BOOL" },
35
- { "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99021, "name": "Latest message time", "short": "", "urcc": "", "unit": "", "fmt": "BOOL" },
36
- { "fam": "xcom", "lvl": "Info", "pnr": 990, "nr": 99022, "name": "Latest message source", "short": "", "urcc": "", "unit": "", "fmt": "BOOL" },
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
- 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.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