PyAutomationIO 2.0.14__tar.gz → 2.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 (187) hide show
  1. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/PKG-INFO +1 -1
  2. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/PyAutomationIO.egg-info/PKG-INFO +1 -1
  3. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/PyAutomationIO.egg-info/SOURCES.txt +5 -1
  4. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/core.py +281 -5
  5. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/dbmodels/__init__.py +2 -1
  6. pyautomationio-2.1.0/automation/dbmodels/linear_referencing_geospatial.py +156 -0
  7. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/dbmodels/tags.py +8 -0
  8. pyautomationio-2.0.14/automation/hmi/assets/index-CFaA_HVS.js → pyautomationio-2.1.0/automation/hmi/assets/index-Bq8WzzWp.js +75 -75
  9. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/hmi/index.html +1 -1
  10. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/logger/datalogger.py +124 -39
  11. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/managers/db.py +2 -0
  12. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/__init__.py +3 -1
  13. pyautomationio-2.1.0/automation/modules/linear_referencing/__init__.py +1 -0
  14. pyautomationio-2.1.0/automation/modules/linear_referencing/resources/__init__.py +7 -0
  15. pyautomationio-2.1.0/automation/modules/linear_referencing/resources/linear_referencing_geospatial.py +246 -0
  16. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/tags/resources/tags.py +35 -3
  17. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/tags/cvt.py +42 -0
  18. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/tags/tag.py +23 -0
  19. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/tests/test_core.py +94 -1
  20. pyautomationio-2.1.0/version.py +2 -0
  21. pyautomationio-2.0.14/version.py +0 -2
  22. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/LICENSE +0 -0
  23. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/MANIFEST.in +0 -0
  24. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/PyAutomationIO.egg-info/dependency_links.txt +0 -0
  25. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/PyAutomationIO.egg-info/requires.txt +0 -0
  26. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/PyAutomationIO.egg-info/top_level.txt +0 -0
  27. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/README.md +0 -0
  28. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/__init__.py +0 -0
  29. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/alarms/__init__.py +0 -0
  30. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/alarms/states.py +0 -0
  31. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/alarms/trigger.py +0 -0
  32. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/buffer.py +0 -0
  33. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/dbmodels/alarms.py +0 -0
  34. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/dbmodels/core.py +0 -0
  35. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/dbmodels/events.py +0 -0
  36. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/dbmodels/logs.py +0 -0
  37. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/dbmodels/machines.py +0 -0
  38. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/dbmodels/opcua.py +0 -0
  39. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/dbmodels/opcua_server.py +0 -0
  40. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/dbmodels/users.py +0 -0
  41. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/extensions/__init__.py +0 -0
  42. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/extensions/api.py +0 -0
  43. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/extensions/cors.py +0 -0
  44. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/filter/__init__.py +0 -0
  45. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/hmi/assets/bootstrap-icons-BeopsB42.woff +0 -0
  46. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/hmi/assets/bootstrap-icons-mSm7cUeB.woff2 +0 -0
  47. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/hmi/assets/fa-brands-400-D1LuMI3I.ttf +0 -0
  48. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/hmi/assets/fa-brands-400-D_cYUPeE.woff2 +0 -0
  49. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/hmi/assets/fa-regular-400-BjRzuEpd.woff2 +0 -0
  50. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/hmi/assets/fa-regular-400-DZaxPHgR.ttf +0 -0
  51. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/hmi/assets/fa-solid-900-CTAAxXor.woff2 +0 -0
  52. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/hmi/assets/fa-solid-900-D0aA9rwL.ttf +0 -0
  53. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/hmi/assets/fa-v4compatibility-C9RhG_FT.woff2 +0 -0
  54. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/hmi/assets/fa-v4compatibility-CCth-dXg.ttf +0 -0
  55. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/hmi/assets/index-fe9Q2n8A.css +0 -0
  56. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/hmi/industrial_pallete_icons/90/302/260 curve 2.svg" +0 -0
  57. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/hmi/industrial_pallete_icons/Analog gauge.svg +0 -0
  58. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/hmi/industrial_pallete_icons/Cool pump.svg +0 -0
  59. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/hmi/industrial_pallete_icons/Hand valve 4.svg +0 -0
  60. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/hmi/industrial_pallete_icons/Mass flowmeter.svg +0 -0
  61. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/hmi/industrial_pallete_icons/Short horizontal pipe.svg +0 -0
  62. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/hmi/industrial_pallete_icons/Tank 16.svg +0 -0
  63. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/iad/__init__.py +0 -0
  64. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/iad/frozen_data.py +0 -0
  65. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/iad/out_of_range.py +0 -0
  66. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/iad/outliers.py +0 -0
  67. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/logger/__init__.py +0 -0
  68. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/logger/alarms.py +0 -0
  69. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/logger/core.py +0 -0
  70. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/logger/events.py +0 -0
  71. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/logger/logdict.py +0 -0
  72. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/logger/logs.py +0 -0
  73. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/logger/machines.py +0 -0
  74. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/logger/opcua_server.py +0 -0
  75. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/logger/users.py +0 -0
  76. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/managers/__init__.py +0 -0
  77. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/managers/alarms.py +0 -0
  78. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/managers/opcua_client.py +0 -0
  79. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/managers/state_machine.py +0 -0
  80. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/models.py +0 -0
  81. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/alarms/__init__.py +0 -0
  82. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/alarms/resources/__init__.py +0 -0
  83. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/alarms/resources/alarms.py +0 -0
  84. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/alarms/resources/summary.py +0 -0
  85. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/database/__init__.py +0 -0
  86. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/database/resources/__init__.py +0 -0
  87. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/database/resources/database.py +0 -0
  88. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/events/__init__.py +0 -0
  89. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/events/resources/__init__.py +0 -0
  90. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/events/resources/events.py +0 -0
  91. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/events/resources/logs.py +0 -0
  92. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/health/__init__.py +0 -0
  93. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/health/resources/__init__.py +0 -0
  94. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/health/resources/health.py +0 -0
  95. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/machines/__init__.py +0 -0
  96. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/machines/resources/__init__.py +0 -0
  97. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/machines/resources/machines.py +0 -0
  98. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/opcua/__init__.py +0 -0
  99. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/opcua/resources/__init__.py +0 -0
  100. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/opcua/resources/clients.py +0 -0
  101. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/opcua/resources/server.py +0 -0
  102. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/settings/__init__.py +0 -0
  103. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/settings/resources/__init__.py +0 -0
  104. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/settings/resources/settings.py +0 -0
  105. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/tags/__init__.py +0 -0
  106. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/tags/resources/__init__.py +0 -0
  107. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/users/__init__.py +0 -0
  108. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/users/resources/__init__.py +0 -0
  109. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/users/resources/models/__init__.py +0 -0
  110. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/users/resources/models/roles.py +0 -0
  111. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/users/resources/models/users.py +0 -0
  112. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/users/resources/roles.py +0 -0
  113. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/users/resources/users.py +0 -0
  114. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/users/roles.py +0 -0
  115. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/modules/users/users.py +0 -0
  116. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/opcua/__init__.py +0 -0
  117. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/opcua/models.py +0 -0
  118. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/opcua/subscription.py +0 -0
  119. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/__init__.py +0 -0
  120. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/alarms.py +0 -0
  121. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/alarms_history.py +0 -0
  122. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/assets/styles.css +0 -0
  123. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/callbacks/__init__.py +0 -0
  124. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/callbacks/alarms.py +0 -0
  125. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/callbacks/alarms_summary.py +0 -0
  126. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/callbacks/db.py +0 -0
  127. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/callbacks/filter.py +0 -0
  128. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/callbacks/machines.py +0 -0
  129. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/callbacks/machines_detailed.py +0 -0
  130. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/callbacks/opcua.py +0 -0
  131. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/callbacks/opcua_server.py +0 -0
  132. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/callbacks/tags.py +0 -0
  133. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/callbacks/trends.py +0 -0
  134. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/communications.py +0 -0
  135. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/components/__init__.py +0 -0
  136. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/components/alarms.py +0 -0
  137. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/components/alarms_summary.py +0 -0
  138. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/components/database.py +0 -0
  139. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/components/gaussian_filter.py +0 -0
  140. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/components/machines.py +0 -0
  141. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/components/opcua.py +0 -0
  142. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/components/opcua_server.py +0 -0
  143. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/components/tags.py +0 -0
  144. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/components/trends.py +0 -0
  145. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/database.py +0 -0
  146. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/filter.py +0 -0
  147. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/machines.py +0 -0
  148. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/machines_detailed.py +0 -0
  149. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/main.py +0 -0
  150. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/opcua_server.py +0 -0
  151. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/tags.py +0 -0
  152. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/pages/trends.py +0 -0
  153. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/singleton.py +0 -0
  154. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/state_machine.py +0 -0
  155. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/tags/__init__.py +0 -0
  156. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/tags/filter.py +0 -0
  157. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/tests/__init__.py +0 -0
  158. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/tests/test_alarms.py +0 -0
  159. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/tests/test_opcua_serialization.py +0 -0
  160. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/tests/test_unit.py +0 -0
  161. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/tests/test_user.py +0 -0
  162. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/utils/__init__.py +0 -0
  163. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/utils/decorators.py +0 -0
  164. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/utils/observer.py +0 -0
  165. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/utils/units.py +0 -0
  166. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/variables/__init__.py +0 -0
  167. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/variables/adimentional.py +0 -0
  168. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/variables/current.py +0 -0
  169. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/variables/density.py +0 -0
  170. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/variables/eng_time.py +0 -0
  171. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/variables/force.py +0 -0
  172. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/variables/length.py +0 -0
  173. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/variables/mass.py +0 -0
  174. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/variables/mass_flow.py +0 -0
  175. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/variables/percentage.py +0 -0
  176. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/variables/power.py +0 -0
  177. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/variables/pressure.py +0 -0
  178. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/variables/temperature.py +0 -0
  179. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/variables/volume.py +0 -0
  180. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/variables/volumetric_flow.py +0 -0
  181. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/workers/__init__.py +0 -0
  182. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/workers/logger.py +0 -0
  183. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/workers/state_machine.py +0 -0
  184. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/automation/workers/worker.py +0 -0
  185. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/requirements.txt +0 -0
  186. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/setup.cfg +0 -0
  187. {pyautomationio-2.0.14 → pyautomationio-2.1.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyAutomationIO
3
- Version: 2.0.14
3
+ Version: 2.1.0
4
4
  Summary: A python framework to develop automation industrial processes applications and Artificial Intelligence applications for the industrial field
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: 2.0.14
3
+ Version: 2.1.0
4
4
  Summary: A python framework to develop automation industrial processes applications and Artificial Intelligence applications for the industrial field
5
5
  Home-page: https://github.com/know-ai/PyAutomation
6
6
  Author: KnowAI
@@ -22,6 +22,7 @@ automation/dbmodels/__init__.py
22
22
  automation/dbmodels/alarms.py
23
23
  automation/dbmodels/core.py
24
24
  automation/dbmodels/events.py
25
+ automation/dbmodels/linear_referencing_geospatial.py
25
26
  automation/dbmodels/logs.py
26
27
  automation/dbmodels/machines.py
27
28
  automation/dbmodels/opcua.py
@@ -43,7 +44,7 @@ automation/hmi/assets/fa-solid-900-CTAAxXor.woff2
43
44
  automation/hmi/assets/fa-solid-900-D0aA9rwL.ttf
44
45
  automation/hmi/assets/fa-v4compatibility-C9RhG_FT.woff2
45
46
  automation/hmi/assets/fa-v4compatibility-CCth-dXg.ttf
46
- automation/hmi/assets/index-CFaA_HVS.js
47
+ automation/hmi/assets/index-Bq8WzzWp.js
47
48
  automation/hmi/assets/index-fe9Q2n8A.css
48
49
  automation/hmi/industrial_pallete_icons/90° curve 2.svg
49
50
  automation/hmi/industrial_pallete_icons/Analog gauge.svg
@@ -86,6 +87,9 @@ automation/modules/events/resources/logs.py
86
87
  automation/modules/health/__init__.py
87
88
  automation/modules/health/resources/__init__.py
88
89
  automation/modules/health/resources/health.py
90
+ automation/modules/linear_referencing/__init__.py
91
+ automation/modules/linear_referencing/resources/__init__.py
92
+ automation/modules/linear_referencing/resources/linear_referencing_geospatial.py
89
93
  automation/modules/machines/__init__.py
90
94
  automation/modules/machines/resources/__init__.py
91
95
  automation/modules/machines/resources/machines.py
@@ -357,6 +357,7 @@ class PyAutomation(Singleton):
357
357
  node_namespace=str|type(None),
358
358
  scan_time=int|float|type(None),
359
359
  dead_band=int|float|type(None),
360
+ kp=int|float|type(None),
360
361
  process_filter=bool,
361
362
  gaussian_filter=bool,
362
363
  gaussian_filter_threshold=float|int,
@@ -393,6 +394,7 @@ class PyAutomation(Singleton):
393
394
  frozen_data_detection:bool=False,
394
395
  segment:str|None="",
395
396
  manufacturer:str|None="",
397
+ kp:float|None=None,
396
398
  id:str=None,
397
399
  user:User|None=None,
398
400
  reload:bool=False,
@@ -490,6 +492,7 @@ class PyAutomation(Singleton):
490
492
  frozen_data_detection=frozen_data_detection,
491
493
  segment=segment,
492
494
  manufacturer=manufacturer,
495
+ kp=kp,
493
496
  id=id,
494
497
  user=user
495
498
  )
@@ -581,6 +584,240 @@ class PyAutomation(Singleton):
581
584
  """
582
585
  return self.cvt.get_tags_filtered(manufacturer=manufacturer, segment=segment)
583
586
 
587
+ @logging_error_handler
588
+ @validate_types(kp_min=int|float, kp_max=int|float, output=list)
589
+ def get_tags_by_kp_range(self, kp_min:float, kp_max:float)->list:
590
+ r"""
591
+ Retrieves tags whose KP is between kp_min and kp_max (inclusive).
592
+
593
+ **Parameters:**
594
+
595
+ * **kp_min** (float): Lower KP bound.
596
+ * **kp_max** (float): Upper KP bound.
597
+
598
+ **Returns:**
599
+
600
+ * **list**: Serialized tags in the requested KP range.
601
+ """
602
+ return self.cvt.get_tags_by_kp_range(kp_min=kp_min, kp_max=kp_max)
603
+
604
+ @logging_error_handler
605
+ @validate_types(segment_name=str, kp=int|float, latitude=int|float, longitude=int|float, elevation=int|float|type(None), output=tuple)
606
+ def create_linear_referencing_geospatial(
607
+ self,
608
+ segment_name:str,
609
+ kp:float,
610
+ latitude:float,
611
+ longitude:float,
612
+ elevation:float|None=None
613
+ )->tuple[dict|None, str]:
614
+ r"""
615
+ Creates a new linear-referencing geospatial point for a segment.
616
+ """
617
+ if not self.is_db_connected():
618
+ return None, "Database not connected"
619
+
620
+ from .dbmodels import LinearReferencingGeospatial
621
+ point, message = LinearReferencingGeospatial.create(
622
+ segment_name=segment_name,
623
+ kp=kp,
624
+ latitude=latitude,
625
+ longitude=longitude,
626
+ elevation=elevation
627
+ )
628
+ if point is None:
629
+ return None, message
630
+ return point.serialize(), message
631
+
632
+ @logging_error_handler
633
+ @validate_types(output=list)
634
+ def get_linear_referencing_geospatial_points(self)->list:
635
+ r"""
636
+ Retrieves all linear-referencing geospatial points.
637
+ """
638
+ if not self.is_db_connected():
639
+ return []
640
+
641
+ from .dbmodels import LinearReferencingGeospatial
642
+ return LinearReferencingGeospatial.read_all()
643
+
644
+ @logging_error_handler
645
+ @validate_types(segment_name=str, output=list)
646
+ def get_linear_referencing_geospatial_points_by_segment(self, segment_name:str)->list:
647
+ r"""
648
+ Retrieves linear-referencing points for a segment ordered by KP.
649
+ """
650
+ if not self.is_db_connected():
651
+ return []
652
+
653
+ from .dbmodels import LinearReferencingGeospatial
654
+ query = LinearReferencingGeospatial.read_by_segment_name(segment_name=segment_name)
655
+ return [point.serialize() for point in query]
656
+
657
+ @logging_error_handler
658
+ @validate_types(id=int, output=dict|None)
659
+ def get_linear_referencing_geospatial_point(self, id:int)->dict|None:
660
+ r"""
661
+ Retrieves one linear-referencing geospatial point by ID.
662
+ """
663
+ if not self.is_db_connected():
664
+ return None
665
+
666
+ from .dbmodels import LinearReferencingGeospatial
667
+ point = LinearReferencingGeospatial.read(id=id)
668
+ if point is None:
669
+ return None
670
+ return point.serialize()
671
+
672
+ @logging_error_handler
673
+ def update_linear_referencing_geospatial_point(self, id:int, **kwargs)->tuple[dict|None, str]:
674
+ r"""
675
+ Updates a linear-referencing geospatial point by ID.
676
+ """
677
+ if not self.is_db_connected():
678
+ return None, "Database not connected"
679
+
680
+ from .dbmodels import LinearReferencingGeospatial, Segment
681
+ point = LinearReferencingGeospatial.read(id=id)
682
+ if point is None:
683
+ return None, f"Linear referencing geospatial point {id} not found"
684
+
685
+ fields = kwargs.copy()
686
+ if "segment_name" in fields:
687
+ segment_name = fields.pop("segment_name")
688
+ segment_obj = Segment.read_by_name(name=segment_name)
689
+ if segment_obj is None:
690
+ return None, f"Segment {segment_name} does not exist into database"
691
+ fields["segment"] = segment_obj
692
+
693
+ if not fields:
694
+ return point.serialize(), "No fields to update"
695
+
696
+ LinearReferencingGeospatial.put(id=id, **fields)
697
+ updated = LinearReferencingGeospatial.read(id=id)
698
+ return updated.serialize(), "Linear referencing geospatial point updated successfully"
699
+
700
+ @logging_error_handler
701
+ @validate_types(id=int, output=tuple)
702
+ def delete_linear_referencing_geospatial_point(self, id:int)->tuple[bool, str]:
703
+ r"""
704
+ Deletes a linear-referencing geospatial point by ID.
705
+ """
706
+ if not self.is_db_connected():
707
+ return False, "Database not connected"
708
+
709
+ from .dbmodels import LinearReferencingGeospatial
710
+ rows = LinearReferencingGeospatial.delete_by_id(id=id)
711
+ if rows == 0:
712
+ return False, f"Linear referencing geospatial point {id} not found"
713
+ return True, "Linear referencing geospatial point deleted successfully"
714
+
715
+ @logging_error_handler
716
+ @validate_types(segment_name=str, kp=int|float, output=tuple)
717
+ def get_geospatial_by_segment_and_kp(self, segment_name:str, kp:float)->tuple[dict|None, str]:
718
+ r"""
719
+ Retrieves geospatial coordinates by segment and KP with interpolation support.
720
+ """
721
+ if not self.is_db_connected():
722
+ return None, "Database not connected"
723
+
724
+ from .dbmodels import LinearReferencingGeospatial
725
+ return LinearReferencingGeospatial.interpolate_by_segment_and_kp(
726
+ segment_name=segment_name,
727
+ kp=kp
728
+ )
729
+
730
+ @logging_error_handler
731
+ @validate_types(rows=list, default_segment_name=str|type(None), update_existing=bool, output=dict)
732
+ def import_linear_referencing_profile(
733
+ self,
734
+ rows:list[dict],
735
+ default_segment_name:str|None=None,
736
+ update_existing:bool=True
737
+ )->dict:
738
+ r"""
739
+ Imports a complete linear-referencing profile from parsed rows (CSV/XLSX).
740
+ """
741
+ result = {
742
+ "created": 0,
743
+ "updated": 0,
744
+ "skipped": 0,
745
+ "errors": []
746
+ }
747
+
748
+ if not self.is_db_connected():
749
+ result["errors"].append("Database not connected")
750
+ return result
751
+
752
+ from .dbmodels import LinearReferencingGeospatial, Segment
753
+
754
+ for idx, row in enumerate(rows, start=1):
755
+ try:
756
+ segment_name = row.get("segment_name") or default_segment_name
757
+ if not segment_name:
758
+ result["errors"].append(f"Row {idx}: missing segment_name")
759
+ continue
760
+
761
+ segment_obj = Segment.read_by_name(name=segment_name)
762
+ if segment_obj is None:
763
+ result["errors"].append(f"Row {idx}: segment {segment_name} does not exist")
764
+ continue
765
+
766
+ if row.get("kp") is None:
767
+ result["errors"].append(f"Row {idx}: missing kp")
768
+ continue
769
+ if row.get("latitude") is None:
770
+ result["errors"].append(f"Row {idx}: missing latitude")
771
+ continue
772
+ if row.get("longitude") is None:
773
+ result["errors"].append(f"Row {idx}: missing longitude")
774
+ continue
775
+
776
+ kp = float(row.get("kp"))
777
+ latitude = float(row.get("latitude"))
778
+ longitude = float(row.get("longitude"))
779
+ elevation = row.get("elevation")
780
+ if elevation in ("", None):
781
+ elevation = None
782
+ else:
783
+ elevation = float(elevation)
784
+
785
+ current = LinearReferencingGeospatial.get_or_none(
786
+ (LinearReferencingGeospatial.segment == segment_obj)
787
+ & (LinearReferencingGeospatial.kp == kp)
788
+ )
789
+
790
+ if current is None:
791
+ point, message = LinearReferencingGeospatial.create(
792
+ segment_name=segment_name,
793
+ kp=kp,
794
+ latitude=latitude,
795
+ longitude=longitude,
796
+ elevation=elevation
797
+ )
798
+ if point is None:
799
+ result["errors"].append(f"Row {idx}: {message}")
800
+ else:
801
+ result["created"] += 1
802
+ else:
803
+ if not update_existing:
804
+ result["skipped"] += 1
805
+ continue
806
+ LinearReferencingGeospatial.put(
807
+ id=current.id,
808
+ latitude=latitude,
809
+ longitude=longitude,
810
+ elevation=elevation
811
+ )
812
+ result["updated"] += 1
813
+
814
+ except Exception as err:
815
+ result["errors"].append(f"Row {idx}: {str(err)}")
816
+
817
+ result["processed"] = len(rows)
818
+ result["success"] = len(result["errors"]) == 0
819
+ return result
820
+
584
821
  @logging_error_handler
585
822
  @validate_types(names=list, output=list)
586
823
  def get_tags_by_names(self, names:list)->list[Tag|None]:
@@ -4274,7 +4511,7 @@ class PyAutomation(Singleton):
4274
4511
 
4275
4512
  Exports configuration tables: Manufacturer, Segment, Variables, Units, DataTypes,
4276
4513
  Tags, AlarmTypes, AlarmStates, Alarms, Roles, Users, OPCUA, AccessType,
4277
- OPCUAServer, Machines, TagsMachines.
4514
+ OPCUAServer, Machines, TagsMachines, LinearReferencingGeospatial.
4278
4515
 
4279
4516
  Excludes historical tables: TagValue, Events, Logs, AlarmSummary.
4280
4517
 
@@ -4301,7 +4538,7 @@ class PyAutomation(Singleton):
4301
4538
  Manufacturer, Segment, Variables, Units, DataTypes,
4302
4539
  Tags, AlarmTypes, AlarmStates, Alarms,
4303
4540
  Roles, Users, OPCUA, AccessType, OPCUAServer,
4304
- Machines, TagsMachines
4541
+ Machines, TagsMachines, LinearReferencingGeospatial
4305
4542
  )
4306
4543
 
4307
4544
  config = {
@@ -4328,7 +4565,8 @@ class PyAutomation(Singleton):
4328
4565
  "AccessType": [at.serialize() for at in AccessType.select()],
4329
4566
  "OPCUAServer": [os_obj.serialize() for os_obj in OPCUAServer.select()],
4330
4567
  "Machines": [m.serialize() for m in Machines.select()],
4331
- "TagsMachines": [tm.serialize() for tm in TagsMachines.select()]
4568
+ "TagsMachines": [tm.serialize() for tm in TagsMachines.select()],
4569
+ "LinearReferencingGeospatial": [lr.serialize() for lr in LinearReferencingGeospatial.select()]
4332
4570
  }
4333
4571
  }
4334
4572
 
@@ -4373,7 +4611,7 @@ class PyAutomation(Singleton):
4373
4611
  Manufacturer, Segment, Variables, Units, DataTypes,
4374
4612
  Tags, AlarmTypes, AlarmStates, Alarms,
4375
4613
  Roles, Users, OPCUA, AccessType, OPCUAServer,
4376
- Machines, TagsMachines
4614
+ Machines, TagsMachines, LinearReferencingGeospatial
4377
4615
  )
4378
4616
 
4379
4617
  results = {
@@ -4503,6 +4741,7 @@ class PyAutomation(Singleton):
4503
4741
  manufacturer=manufacturer_name or "",
4504
4742
  scan_time=item.get("scan_time"),
4505
4743
  dead_band=item.get("dead_band"),
4744
+ kp=item.get("kp"),
4506
4745
  active=item.get("active", True),
4507
4746
  process_filter=item.get("process_filter", False),
4508
4747
  gaussian_filter=item.get("gaussian_filter", False),
@@ -4520,7 +4759,44 @@ class PyAutomation(Singleton):
4520
4759
  except Exception as e:
4521
4760
  results["errors"].setdefault("Tags", []).append(f"{item.get('name', 'unknown')}: {str(e)}")
4522
4761
 
4523
- # 7. AlarmTypes (no dependencies)
4762
+ # 7. LinearReferencingGeospatial (depends on Segment)
4763
+ if "LinearReferencingGeospatial" in data:
4764
+ for item in data["LinearReferencingGeospatial"]:
4765
+ try:
4766
+ segment_name = item.get("segment_name")
4767
+ if not segment_name:
4768
+ results["errors"].setdefault("LinearReferencingGeospatial", []).append(f"{item.get('id', 'unknown')}: Missing segment_name")
4769
+ continue
4770
+
4771
+ segment_obj = Segment.read_by_name(segment_name)
4772
+ if segment_obj is None:
4773
+ results["errors"].setdefault("LinearReferencingGeospatial", []).append(f"{item.get('id', 'unknown')}: Segment {segment_name} not found")
4774
+ continue
4775
+
4776
+ exists = LinearReferencingGeospatial.get_or_none(
4777
+ (LinearReferencingGeospatial.segment == segment_obj)
4778
+ & (LinearReferencingGeospatial.kp == item.get("kp"))
4779
+ )
4780
+ if exists is None:
4781
+ point, message = LinearReferencingGeospatial.create(
4782
+ segment_name=segment_name,
4783
+ kp=item.get("kp"),
4784
+ latitude=item.get("latitude"),
4785
+ longitude=item.get("longitude"),
4786
+ elevation=item.get("elevation")
4787
+ )
4788
+ if point is None:
4789
+ results["errors"].setdefault("LinearReferencingGeospatial", []).append(f"{item.get('id', 'unknown')}: {message}")
4790
+ else:
4791
+ results["imported"].setdefault("LinearReferencingGeospatial", 0)
4792
+ results["imported"]["LinearReferencingGeospatial"] += 1
4793
+ else:
4794
+ results["skipped"].setdefault("LinearReferencingGeospatial", 0)
4795
+ results["skipped"]["LinearReferencingGeospatial"] += 1
4796
+ except Exception as e:
4797
+ results["errors"].setdefault("LinearReferencingGeospatial", []).append(f"{item.get('id', 'unknown')}: {str(e)}")
4798
+
4799
+ # 8. AlarmTypes (no dependencies)
4524
4800
  if "AlarmTypes" in data:
4525
4801
  for item in data["AlarmTypes"]:
4526
4802
  try:
@@ -20,4 +20,5 @@ from .users import Roles, Users
20
20
  from .events import Events
21
21
  from .logs import Logs
22
22
  from .machines import Machines, TagsMachines
23
- from .opcua_server import AccessType, OPCUAServer
23
+ from .opcua_server import AccessType, OPCUAServer
24
+ from .linear_referencing_geospatial import LinearReferencingGeospatial
@@ -0,0 +1,156 @@
1
+ from peewee import ForeignKeyField, FloatField
2
+ from .core import BaseModel
3
+ from .tags import Segment
4
+
5
+
6
+ class LinearReferencingGeospatial(BaseModel):
7
+ r"""
8
+ Database model for geospatial linear-referencing points of a pipeline segment.
9
+ """
10
+
11
+ segment = ForeignKeyField(Segment, backref='linear_referencing_points', on_delete='CASCADE')
12
+ kp = FloatField()
13
+ latitude = FloatField()
14
+ longitude = FloatField()
15
+ elevation = FloatField(null=True)
16
+
17
+ class Meta:
18
+ indexes = (
19
+ (('segment', 'kp'), True),
20
+ )
21
+
22
+ @classmethod
23
+ def create(
24
+ cls,
25
+ segment_name:str,
26
+ kp:float,
27
+ latitude:float,
28
+ longitude:float,
29
+ elevation:float=None
30
+ ):
31
+ r"""
32
+ Creates a new linear-referencing geospatial point.
33
+ """
34
+ segment_obj = Segment.read_by_name(name=segment_name)
35
+ if segment_obj is None:
36
+ return None, f"Segment {segment_name} does not exist into database"
37
+
38
+ duplicated = cls.get_or_none((cls.segment == segment_obj) & (cls.kp == kp))
39
+ if duplicated is not None:
40
+ return None, f"Duplicated linear referencing point: {segment_name} KP {kp}"
41
+
42
+ query = cls(
43
+ segment=segment_obj,
44
+ kp=kp,
45
+ latitude=latitude,
46
+ longitude=longitude,
47
+ elevation=elevation
48
+ )
49
+ query.save()
50
+ return query, "Linear referencing geospatial point created successfully"
51
+
52
+ @classmethod
53
+ def read_by_segment_name(cls, segment_name:str):
54
+ r"""
55
+ Retrieves all points by segment name ordered by KP ascending.
56
+ """
57
+ segment_obj = Segment.read_by_name(name=segment_name)
58
+ if segment_obj is None:
59
+ return []
60
+ query = cls.select().where(cls.segment == segment_obj).order_by(cls.kp.asc())
61
+ return query
62
+
63
+ @classmethod
64
+ def delete_by_id(cls, id:int):
65
+ r"""
66
+ Deletes a point by its numeric ID.
67
+ """
68
+ if not cls.id_exists(id):
69
+ return 0
70
+ query = cls.delete().where(cls.id == id)
71
+ return query.execute()
72
+
73
+ @classmethod
74
+ def interpolate_by_segment_and_kp(cls, segment_name:str, kp:float):
75
+ r"""
76
+ Retrieves geospatial data by segment and KP using linear interpolation if needed.
77
+ """
78
+ segment_obj = Segment.read_by_name(name=segment_name)
79
+ if segment_obj is None:
80
+ return None, f"Segment {segment_name} does not exist into database"
81
+
82
+ exact = cls.get_or_none((cls.segment == segment_obj) & (cls.kp == kp))
83
+ if exact is not None:
84
+ data = exact.serialize()
85
+ data["interpolated"] = False
86
+ return data, "Exact KP match"
87
+
88
+ lower = (
89
+ cls.select()
90
+ .where((cls.segment == segment_obj) & (cls.kp < kp))
91
+ .order_by(cls.kp.desc())
92
+ .first()
93
+ )
94
+ upper = (
95
+ cls.select()
96
+ .where((cls.segment == segment_obj) & (cls.kp > kp))
97
+ .order_by(cls.kp.asc())
98
+ .first()
99
+ )
100
+
101
+ if lower is None and upper is None:
102
+ return None, f"No geospatial points configured for segment {segment_name}"
103
+
104
+ if lower is None:
105
+ data = upper.serialize()
106
+ data["requested_kp"] = kp
107
+ data["interpolated"] = False
108
+ data["interpolation_note"] = "Requested KP below available range; nearest upper point returned"
109
+ return data, "Nearest point returned"
110
+
111
+ if upper is None:
112
+ data = lower.serialize()
113
+ data["requested_kp"] = kp
114
+ data["interpolated"] = False
115
+ data["interpolation_note"] = "Requested KP above available range; nearest lower point returned"
116
+ return data, "Nearest point returned"
117
+
118
+ kp_span = upper.kp - lower.kp
119
+ if kp_span == 0:
120
+ return None, "Invalid interpolation range (duplicated KP points)"
121
+
122
+ ratio = (kp - lower.kp) / kp_span
123
+ latitude = lower.latitude + (upper.latitude - lower.latitude) * ratio
124
+ longitude = lower.longitude + (upper.longitude - lower.longitude) * ratio
125
+ elevation = None
126
+ if lower.elevation is not None and upper.elevation is not None:
127
+ elevation = lower.elevation + (upper.elevation - lower.elevation) * ratio
128
+
129
+ return {
130
+ "id": None,
131
+ "segment_id": segment_obj.id,
132
+ "segment_name": segment_obj.name,
133
+ "kp": kp,
134
+ "latitude": latitude,
135
+ "longitude": longitude,
136
+ "elevation": elevation,
137
+ "interpolated": True,
138
+ "interpolation_bounds": {
139
+ "lower_kp": lower.kp,
140
+ "upper_kp": upper.kp
141
+ }
142
+ }, "Interpolated KP location"
143
+
144
+ def serialize(self):
145
+ r"""
146
+ Serializes the linear-referencing geospatial point.
147
+ """
148
+ return {
149
+ "id": self.id,
150
+ "segment_id": self.segment.id,
151
+ "segment_name": self.segment.name,
152
+ "kp": self.kp,
153
+ "latitude": self.latitude,
154
+ "longitude": self.longitude,
155
+ "elevation": self.elevation
156
+ }
@@ -434,6 +434,7 @@ class Tags(BaseModel):
434
434
  node_namespace = CharField(null=True)
435
435
  scan_time = IntegerField(null=True)
436
436
  dead_band = FloatField(null=True)
437
+ kp = FloatField(null=True)
437
438
  active = BooleanField(default=True)
438
439
  process_filter = BooleanField(default=False)
439
440
  gaussian_filter = BooleanField(default=False)
@@ -460,6 +461,7 @@ class Tags(BaseModel):
460
461
  manufacturer:str="",
461
462
  scan_time:int=0,
462
463
  dead_band:float=0.0,
464
+ kp:float=None,
463
465
  active:bool=True,
464
466
  process_filter:bool=False,
465
467
  gaussian_filter:bool=False,
@@ -535,6 +537,7 @@ class Tags(BaseModel):
535
537
  node_namespace=node_namespace,
536
538
  scan_time=scan_time,
537
539
  dead_band=dead_band,
540
+ kp=kp,
538
541
  active=active,
539
542
  process_filter=process_filter,
540
543
  gaussian_filter=gaussian_filter,
@@ -559,6 +562,7 @@ class Tags(BaseModel):
559
562
  node_namespace=node_namespace,
560
563
  scan_time=scan_time,
561
564
  dead_band=dead_band,
565
+ kp=kp,
562
566
  active=active,
563
567
  process_filter=process_filter,
564
568
  gaussian_filter=gaussian_filter,
@@ -617,6 +621,7 @@ class Tags(BaseModel):
617
621
  "node_namespace":node_namespace,
618
622
  "scan_time":scan_time,
619
623
  "dead_band":dead_band,
624
+ "kp":kp,
620
625
  "active": True
621
626
  }
622
627
  cls.put(id=tag.id, **payload)
@@ -790,6 +795,7 @@ class Tags(BaseModel):
790
795
  'node_namespace': self.node_namespace,
791
796
  'scan_time': self.scan_time,
792
797
  'dead_band': self.dead_band,
798
+ 'kp': self.kp,
793
799
  'variable': self.unit.variable_id.name,
794
800
  'active': self.active,
795
801
  'process_filter': self.process_filter,
@@ -817,6 +823,8 @@ class TagValue(BaseModel):
817
823
  class Meta:
818
824
  indexes = (
819
825
  (('timestamp',), False),
826
+ # Consultas típicas de tendencias: por tag + rango de tiempo
827
+ (('tag', 'timestamp'), False),
820
828
  )
821
829
 
822
830
  @classmethod