PyAutomationIO 1.0.3__tar.gz → 1.1.0__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 (147) hide show
  1. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/PKG-INFO +1 -1
  2. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/PyAutomationIO.egg-info/PKG-INFO +1 -1
  3. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/core.py +7 -0
  4. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/logger/datalogger.py +184 -3
  5. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/modules/tags/resources/tags.py +53 -0
  6. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/state_machine.py +2 -0
  7. pyautomationio-1.1.0/version.py +2 -0
  8. pyautomationio-1.0.3/version.py +0 -2
  9. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/LICENSE +0 -0
  10. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/MANIFEST.in +0 -0
  11. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/PyAutomationIO.egg-info/SOURCES.txt +0 -0
  12. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/PyAutomationIO.egg-info/dependency_links.txt +0 -0
  13. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/PyAutomationIO.egg-info/requires.txt +0 -0
  14. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/PyAutomationIO.egg-info/top_level.txt +0 -0
  15. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/README.md +0 -0
  16. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/__init__.py +0 -0
  17. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/alarms/__init__.py +0 -0
  18. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/alarms/states.py +0 -0
  19. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/alarms/trigger.py +0 -0
  20. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/buffer.py +0 -0
  21. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/dbmodels/__init__.py +0 -0
  22. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/dbmodels/alarms.py +0 -0
  23. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/dbmodels/core.py +0 -0
  24. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/dbmodels/events.py +0 -0
  25. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/dbmodels/logs.py +0 -0
  26. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/dbmodels/machines.py +0 -0
  27. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/dbmodels/opcua.py +0 -0
  28. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/dbmodels/opcua_server.py +0 -0
  29. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/dbmodels/tags.py +0 -0
  30. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/dbmodels/users.py +0 -0
  31. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/extensions/__init__.py +0 -0
  32. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/extensions/api.py +0 -0
  33. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/extensions/cors.py +0 -0
  34. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/filter/__init__.py +0 -0
  35. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/iad/__init__.py +0 -0
  36. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/iad/frozen_data.py +0 -0
  37. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/iad/out_of_range.py +0 -0
  38. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/iad/outliers.py +0 -0
  39. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/logger/__init__.py +0 -0
  40. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/logger/alarms.py +0 -0
  41. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/logger/core.py +0 -0
  42. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/logger/events.py +0 -0
  43. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/logger/logdict.py +0 -0
  44. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/logger/logs.py +0 -0
  45. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/logger/machines.py +0 -0
  46. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/logger/opcua_server.py +0 -0
  47. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/logger/users.py +0 -0
  48. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/managers/__init__.py +0 -0
  49. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/managers/alarms.py +0 -0
  50. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/managers/db.py +0 -0
  51. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/managers/opcua_client.py +0 -0
  52. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/managers/state_machine.py +0 -0
  53. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/models.py +0 -0
  54. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/modules/__init__.py +0 -0
  55. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/modules/alarms/__init__.py +0 -0
  56. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/modules/alarms/resources/__init__.py +0 -0
  57. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/modules/alarms/resources/alarms.py +0 -0
  58. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/modules/alarms/resources/summary.py +0 -0
  59. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/modules/events/__init__.py +0 -0
  60. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/modules/events/resources/__init__.py +0 -0
  61. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/modules/events/resources/events.py +0 -0
  62. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/modules/events/resources/logs.py +0 -0
  63. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/modules/tags/__init__.py +0 -0
  64. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/modules/tags/resources/__init__.py +0 -0
  65. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/modules/users/__init__.py +0 -0
  66. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/modules/users/resources/__init__.py +0 -0
  67. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/modules/users/resources/models/__init__.py +0 -0
  68. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/modules/users/resources/models/roles.py +0 -0
  69. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/modules/users/resources/models/users.py +0 -0
  70. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/modules/users/resources/roles.py +0 -0
  71. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/modules/users/resources/users.py +0 -0
  72. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/modules/users/roles.py +0 -0
  73. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/modules/users/users.py +0 -0
  74. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/opcua/__init__.py +0 -0
  75. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/opcua/models.py +0 -0
  76. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/opcua/subscription.py +0 -0
  77. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/__init__.py +0 -0
  78. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/alarms.py +0 -0
  79. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/alarms_history.py +0 -0
  80. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/assets/styles.css +0 -0
  81. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/callbacks/__init__.py +0 -0
  82. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/callbacks/alarms.py +0 -0
  83. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/callbacks/alarms_summary.py +0 -0
  84. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/callbacks/db.py +0 -0
  85. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/callbacks/filter.py +0 -0
  86. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/callbacks/machines.py +0 -0
  87. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/callbacks/machines_detailed.py +0 -0
  88. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/callbacks/opcua.py +0 -0
  89. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/callbacks/opcua_server.py +0 -0
  90. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/callbacks/tags.py +0 -0
  91. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/callbacks/trends.py +0 -0
  92. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/communications.py +0 -0
  93. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/components/__init__.py +0 -0
  94. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/components/alarms.py +0 -0
  95. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/components/alarms_summary.py +0 -0
  96. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/components/database.py +0 -0
  97. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/components/gaussian_filter.py +0 -0
  98. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/components/machines.py +0 -0
  99. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/components/opcua.py +0 -0
  100. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/components/opcua_server.py +0 -0
  101. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/components/tags.py +0 -0
  102. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/components/trends.py +0 -0
  103. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/database.py +0 -0
  104. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/filter.py +0 -0
  105. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/machines.py +0 -0
  106. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/machines_detailed.py +0 -0
  107. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/main.py +0 -0
  108. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/opcua_server.py +0 -0
  109. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/tags.py +0 -0
  110. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/pages/trends.py +0 -0
  111. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/singleton.py +0 -0
  112. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/tags/__init__.py +0 -0
  113. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/tags/cvt.py +0 -0
  114. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/tags/filter.py +0 -0
  115. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/tags/tag.py +0 -0
  116. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/tests/__init__.py +0 -0
  117. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/tests/test_alarms.py +0 -0
  118. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/tests/test_core.py +0 -0
  119. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/tests/test_unit.py +0 -0
  120. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/tests/test_user.py +0 -0
  121. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/utils/__init__.py +0 -0
  122. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/utils/decorators.py +0 -0
  123. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/utils/npw.py +0 -0
  124. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/utils/observer.py +0 -0
  125. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/utils/units.py +0 -0
  126. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/variables/__init__.py +0 -0
  127. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/variables/adimentional.py +0 -0
  128. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/variables/current.py +0 -0
  129. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/variables/density.py +0 -0
  130. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/variables/eng_time.py +0 -0
  131. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/variables/force.py +0 -0
  132. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/variables/length.py +0 -0
  133. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/variables/mass.py +0 -0
  134. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/variables/mass_flow.py +0 -0
  135. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/variables/percentage.py +0 -0
  136. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/variables/power.py +0 -0
  137. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/variables/pressure.py +0 -0
  138. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/variables/temperature.py +0 -0
  139. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/variables/volume.py +0 -0
  140. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/variables/volumetric_flow.py +0 -0
  141. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/workers/__init__.py +0 -0
  142. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/workers/logger.py +0 -0
  143. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/workers/state_machine.py +0 -0
  144. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/automation/workers/worker.py +0 -0
  145. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/requirements.txt +0 -0
  146. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/setup.cfg +0 -0
  147. {pyautomationio-1.0.3 → pyautomationio-1.1.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyAutomationIO
3
- Version: 1.0.3
3
+ Version: 1.1.0
4
4
  Summary: A python library to develop automation continuous tasks using sync or async concurrent threads
5
5
  Home-page: https://github.com/know-ai/PyAutomation
6
6
  Author: KnowAI
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyAutomationIO
3
- Version: 1.0.3
3
+ Version: 1.1.0
4
4
  Summary: A python library to develop automation continuous tasks using sync or async concurrent threads
5
5
  Home-page: https://github.com/know-ai/PyAutomation
6
6
  Author: KnowAI
@@ -373,6 +373,13 @@ class PyAutomation(Singleton):
373
373
  """
374
374
  return self.logger_engine.read_table(start, stop, timezone, tags, page, limit)
375
375
 
376
+ @logging_error_handler
377
+ def get_tabular_data(self, start:str, stop:str, timezone:str, tags:list, sample_time:int, page:int=1, limit:int=20):
378
+ r"""
379
+ Documentation here
380
+ """
381
+ return self.logger_engine.read_tabular_data(start, stop, timezone, tags, sample_time, page, limit)
382
+
376
383
  @logging_error_handler
377
384
  def get_segments(self):
378
385
  r"""
@@ -388,6 +388,186 @@ class DataLogger(BaseLogger):
388
388
  }
389
389
  }
390
390
 
391
+ @db_rollback
392
+ def read_tabular_data(self, start:str, stop:str, timezone:str, tags:list, sample_time:int, page:int=1, limit:int=20):
393
+ r"""
394
+ Get historical data in tabular format with pagination and forward-fill resampling.
395
+
396
+ Args:
397
+ start (str): Start datetime string
398
+ stop (str): Stop datetime string
399
+ timezone (str): Timezone string
400
+ tags (list): List of tag names
401
+ sample_time (int): Sample time in seconds
402
+ page (int): Page number
403
+ limit (int): Items per page
404
+
405
+ Returns:
406
+ dict: {
407
+ "data": [...],
408
+ "pagination": {...}
409
+ }
410
+ """
411
+ if not self.is_history_logged:
412
+ return None
413
+
414
+ if not self.check_connectivity():
415
+ return dict()
416
+
417
+ _timezone = pytz.timezone(timezone)
418
+ utc_timezone = pytz.UTC
419
+
420
+ try:
421
+ start_dt = _timezone.localize(datetime.strptime(start, DATETIME_FORMAT)).astimezone(utc_timezone)
422
+ stop_dt = _timezone.localize(datetime.strptime(stop, DATETIME_FORMAT)).astimezone(utc_timezone)
423
+ start_ts = start_dt.timestamp()
424
+ stop_ts = stop_dt.timestamp()
425
+ except ValueError:
426
+ return dict()
427
+
428
+ # Calculate total records based on time range and sample time
429
+ total_duration = stop_ts - start_ts
430
+ if total_duration < 0:
431
+ return {"data": [], "pagination": {}}
432
+
433
+ total_records = math.floor(total_duration / sample_time) + 1
434
+
435
+ # Pagination calculations
436
+ if limit <= 0: limit = 20
437
+ if page <= 0: page = 1
438
+
439
+ total_pages = math.ceil(total_records / limit)
440
+ if total_pages == 0: total_pages = 1
441
+
442
+ has_next = page < total_pages
443
+ has_prev = page > 1
444
+
445
+ # Calculate start and end for current page
446
+ start_index = (page - 1) * limit
447
+ end_index = min(start_index + limit, total_records)
448
+
449
+ page_start_ts = start_ts + (start_index * sample_time)
450
+ page_end_ts = start_ts + ((end_index - 1) * sample_time)
451
+
452
+ # Query data needed for this page plus context for forward fill
453
+ # We need data up to page_end_ts.
454
+ # For forward fill, we need the last known value before or at page_start_ts for each tag.
455
+
456
+ data_points = []
457
+ current_ts = page_start_ts
458
+
459
+ # Pre-fetch data for optimization could be complex due to forward fill requirement across large gaps.
460
+ # However, we can query per tag or query all data in range.
461
+ # For efficiency with many tags/large range, we should query intelligently.
462
+ # But to guarantee "last known value", we might need to look back indefinitely if no recent data.
463
+ # A simple approach for now: Query "last value before or at page_start_ts" for each tag,
464
+ # and all values between page_start_ts and page_end_ts.
465
+
466
+ # 1. Get initial values (state at page_start_ts)
467
+ current_values = {}
468
+ # Convert float timestamp to datetime for Peewee comparison
469
+ current_dt = datetime.fromtimestamp(current_ts, pytz.UTC)
470
+
471
+ for tag_name in tags:
472
+ # Get the latest value <= current_dt
473
+ last_val_query = (TagValue
474
+ .select(TagValue.value)
475
+ .join(Tags)
476
+ .where((Tags.name == tag_name) & (TagValue.timestamp <= current_dt))
477
+ .order_by(TagValue.timestamp.desc())
478
+ .limit(1)
479
+ .dicts())
480
+
481
+ entry = list(last_val_query)
482
+ if entry:
483
+ current_values[tag_name] = entry[0]['value']
484
+ else:
485
+ current_values[tag_name] = None
486
+
487
+ # 2. Get changes within the page window
488
+ # We query all changes for these tags in the time window of the page
489
+
490
+ # Convert boundaries to datetime for Peewee
491
+ page_start_dt = datetime.fromtimestamp(page_start_ts, pytz.UTC)
492
+ page_end_dt = datetime.fromtimestamp(page_end_ts, pytz.UTC)
493
+
494
+ changes_query = (TagValue
495
+ .select(Tags.name, TagValue.value, TagValue.timestamp)
496
+ .join(Tags)
497
+ .where(
498
+ (Tags.name.in_(tags)) &
499
+ (TagValue.timestamp > page_start_dt) &
500
+ (TagValue.timestamp <= page_end_dt)
501
+ )
502
+ .order_by(TagValue.timestamp.asc())
503
+ .dicts())
504
+
505
+ # Organize changes by timestamp
506
+ changes_by_ts = defaultdict(dict)
507
+ for change in changes_query:
508
+ # timestamp comes as datetime from Peewee
509
+ ts_val = change['timestamp']
510
+ if isinstance(ts_val, datetime):
511
+ # Ensure it's timezone aware or treat as UTC if naive
512
+ if ts_val.tzinfo is None:
513
+ ts_val = utc_timezone.localize(ts_val)
514
+ ts = ts_val.timestamp()
515
+ else:
516
+ ts = float(ts_val)
517
+
518
+ changes_by_ts[ts][change['name']] = change['value']
519
+
520
+ # 3. Generate tabular data
521
+ # We iterate step by step. This might be slow if step is small and range is large,
522
+ # but we are limited by pagination 'limit' (e.g. 20 rows), so it's fast!
523
+
524
+ # We need to process from page_start_ts to page_end_ts in sample_time steps.
525
+ # BUT we have a list of changes.
526
+ # The simple way: Iterate steps.
527
+
528
+ # Optimization: We have 'limit' steps.
529
+
530
+ changes_iter = sorted(changes_by_ts.keys())
531
+ change_idx = 0
532
+
533
+ for i in range(end_index - start_index):
534
+ step_ts = page_start_ts + (i * sample_time)
535
+
536
+ # Update current_values with any changes that happened between last step and now (inclusive)
537
+ # Actually, standard sample-hold means at time T we have value at T.
538
+ # If multiple values in (T-1, T], usually the last one prevails or the one at T?
539
+ # Requirement: "retorne exactamente el timestamp... valor anterior mas cercano registrado" (forward fill)
540
+ # So at step_ts, value is the latest value where timestamp <= step_ts.
541
+
542
+ # Advance change_idx to consume all changes <= step_ts
543
+ while change_idx < len(changes_iter) and changes_iter[change_idx] <= step_ts:
544
+ ts = changes_iter[change_idx]
545
+ for tag, val in changes_by_ts[ts].items():
546
+ current_values[tag] = val
547
+ change_idx += 1
548
+
549
+ # Build row
550
+ dt_object = datetime.fromtimestamp(step_ts, pytz.UTC)
551
+ formatted_ts = dt_object.astimezone(_timezone).strftime(DATETIME_FORMAT)
552
+
553
+ row = {"timestamp": formatted_ts}
554
+ for tag in tags:
555
+ row[tag] = current_values.get(tag) # None if no value ever recorded
556
+
557
+ data_points.append(row)
558
+
559
+ return {
560
+ "data": data_points,
561
+ "pagination": {
562
+ "page": page,
563
+ "limit": limit,
564
+ "total_records": total_records,
565
+ "total_pages": total_pages,
566
+ "has_next": has_next,
567
+ "has_prev": has_prev
568
+ }
569
+ }
570
+
391
571
  def _agregate_data_every_seconds(self, query, result, seconds:int, timezone:str="UTC"):
392
572
  r"""Documentation here
393
573
  """
@@ -620,17 +800,18 @@ class DataLoggerEngine(BaseEngine):
620
800
  _query["parameters"]["tags"] = tags
621
801
  return self.query(_query)
622
802
 
623
- def read_table(self, start:str, stop:str, timezone:str, tags:list, page:int=1, limit:int=20):
803
+ def read_tabular_data(self, start:str, stop:str, timezone:str, tags:list, sample_time:int, page:int=1, limit:int=20):
624
804
  r"""
625
- Get historical data in table format with pagination on a thread-safe mechanism
805
+ Get historical data in tabular format with pagination on a thread-safe mechanism
626
806
  """
627
807
  _query = dict()
628
- _query["action"] = "read_table"
808
+ _query["action"] = "read_tabular_data"
629
809
  _query["parameters"] = dict()
630
810
  _query["parameters"]["start"] = start
631
811
  _query["parameters"]["stop"] = stop
632
812
  _query["parameters"]["timezone"] = timezone
633
813
  _query["parameters"]["tags"] = tags
814
+ _query["parameters"]["sample_time"] = sample_time
634
815
  _query["parameters"]["page"] = page
635
816
  _query["parameters"]["limit"] = limit
636
817
  return self.query(_query)
@@ -25,6 +25,16 @@ query_table_model = api.model("query_table_model",{
25
25
  'limit': fields.Integer(required=False, default=20, description='Items per page')
26
26
  })
27
27
 
28
+ query_tabular_data_model = api.model("query_tabular_data_model",{
29
+ 'tags': fields.List(fields.String(), required=True),
30
+ 'greater_than_timestamp': fields.DateTime(required=True, default=datetime.now(pytz.utc).astimezone(TIMEZONE) - timedelta(minutes=30), description='Greater than DateTime'),
31
+ 'less_than_timestamp': fields.DateTime(required=True, default=datetime.now(pytz.utc).astimezone(TIMEZONE), description='Less than DateTime'),
32
+ 'sample_time': fields.Integer(required=True, description='Sample time in seconds'),
33
+ 'timezone': fields.String(required=True, default=_TIMEZONE),
34
+ 'page': fields.Integer(required=False, default=1, description='Page number'),
35
+ 'limit': fields.Integer(required=False, default=20, description='Items per page')
36
+ })
37
+
28
38
  write_value_model = api.model("write_value_model", {
29
39
  'tag_name': fields.String(required=True, description='Nombre del tag'),
30
40
  'value': fields.Raw(required=True, description='Valor a escribir (float, int, bool, str)')
@@ -134,6 +144,49 @@ class QueryTableResource(Resource):
134
144
 
135
145
  return result, 200
136
146
 
147
+ @ns.route('/get_tabular_data')
148
+ class GetTabularDataResource(Resource):
149
+
150
+ @api.doc(security='apikey')
151
+ @Api.token_required(auth=True)
152
+ @ns.expect(query_tabular_data_model)
153
+ def post(self):
154
+ """
155
+ Query tag values in tabular format with pagination and resampling.
156
+
157
+ The result contains data points at regular intervals (sample_time) from greater_than_timestamp
158
+ up to less_than_timestamp. If exact data is missing, the previous known value is used (forward fill).
159
+
160
+ Authorized Roles: {0}
161
+ """
162
+ timezone = _TIMEZONE
163
+ tags = api.payload['tags']
164
+ page = api.payload.get('page', 1)
165
+ limit = api.payload.get('limit', 20)
166
+ sample_time = api.payload.get('sample_time', 60) # Default 1 min if not provided, but model requires it
167
+
168
+ if "timezone" in api.payload:
169
+ timezone = api.payload["timezone"]
170
+
171
+ if timezone not in pytz.all_timezones:
172
+ return f"Invalid Timezone", 400
173
+
174
+ for tag in tags:
175
+ if not app.get_tag_by_name(name=tag):
176
+ return f"{tag} not exist into db", 404
177
+
178
+ separator = '.'
179
+ greater_than_timestamp = api.payload['greater_than_timestamp']
180
+ # Ensure timestamp format is consistent
181
+ start = greater_than_timestamp.replace("T", " ").split(separator, 1)[0] + '.00'
182
+
183
+ less_than_timestamp = api.payload['less_than_timestamp']
184
+ stop = less_than_timestamp.replace("T", " ").split(separator, 1)[0] + '.00'
185
+
186
+ result = app.get_tabular_data(start, stop, timezone, tags, sample_time, page, limit)
187
+
188
+ return result, 200
189
+
137
190
  @ns.route('/write_value')
138
191
  class WriteValueResource(Resource):
139
192
 
@@ -457,6 +457,8 @@ class StateMachineCore(StateMachine):
457
457
  self.transitions = transitions
458
458
  self.manufacturer = MANUFACTURER
459
459
  self.segment = SEGMENT
460
+ self.mass_flow_unit_base = "kg/sec"
461
+ self.volumetric_flow_unit_base = "m3/sec"
460
462
  super(StateMachineCore, self).__init__()
461
463
 
462
464
  # State Methods
@@ -0,0 +1,2 @@
1
+ __version__ = '1.1.0'
2
+
@@ -1,2 +0,0 @@
1
- __version__ = '1.0.3'
2
-
File without changes
File without changes
File without changes
File without changes