db.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. """Database connection, PKS batch mapping and alarm-threshold configuration."""
  2. from __future__ import annotations
  3. import re
  4. import pymysql
  5. from .settings import settings
  6. SITE_POINT_COLUMNS = (
  7. "ItemName, ItemDescription, Units, "
  8. "AlarmType1, AlarmType2, AlarmType3, AlarmType4, "
  9. "AlarmLimit1, AlarmLimit2, AlarmLimit3, AlarmLimit4"
  10. )
  11. def connect():
  12. return pymysql.connect(
  13. host=settings.db_host,
  14. port=settings.db_port,
  15. user=settings.db_user,
  16. password=settings.db_password,
  17. database=settings.db_name,
  18. charset="utf8mb4",
  19. cursorclass=pymysql.cursors.DictCursor,
  20. connect_timeout=settings.db_connect_timeout,
  21. read_timeout=600,
  22. write_timeout=60,
  23. autocommit=True,
  24. )
  25. def number(value):
  26. """Coerce a value to a finite float or ``None``."""
  27. import math
  28. if value is None:
  29. return None
  30. try:
  31. value = float(value)
  32. except (TypeError, ValueError):
  33. return None
  34. return value if math.isfinite(value) else None
  35. def load_pks_batches() -> dict[int, str]:
  36. """Resolve PKS import batches to units from the import metadata table."""
  37. connection = connect()
  38. try:
  39. with connection.cursor() as cursor:
  40. cursor.execute(
  41. """
  42. SELECT id, directory_path
  43. FROM compressor_data_import
  44. WHERE data_type = %s
  45. AND directory_path IS NOT NULL
  46. ORDER BY id
  47. """,
  48. ("PKS",),
  49. )
  50. result = {}
  51. for row in cursor.fetchall():
  52. match = re.search(r"(?<!\d)([789])\s*#", str(row["directory_path"]))
  53. if match:
  54. result[int(row["id"])] = f"{match.group(1)}号机"
  55. finally:
  56. connection.close()
  57. if not result:
  58. raise RuntimeError("compressor_data_import 中没有可识别 7#/8#/9# 的 PKS 导入批次")
  59. return result
  60. def alarm_entry(item: dict, alarm_type: str) -> tuple[float | None, str]:
  61. for index in range(1, 5):
  62. configured_type = item.get(f"AlarmType{index}")
  63. if configured_type is not None and str(configured_type).strip() == alarm_type:
  64. return number(item.get(f"AlarmLimit{index}")), f"AlarmType{index}/AlarmLimit{index}"
  65. return None, ""
  66. def load_alarm_config(batch_to_unit: dict[int, str]) -> dict[int, dict]:
  67. """Read alarm limits by AlarmType, because AlarmLimit positions differ."""
  68. result = {}
  69. connection = connect()
  70. try:
  71. with connection.cursor() as cursor:
  72. unit_numbers = sorted({unit[0] for unit in batch_to_unit.values()})
  73. names = [f"YSJ{unit}_{point}" for unit in unit_numbers for point in (5, 10, 11)]
  74. placeholders = ",".join(["%s"] * len(names))
  75. cursor.execute(
  76. f"SELECT {SITE_POINT_COLUMNS} FROM site_point WHERE ItemName IN ({placeholders})",
  77. names,
  78. )
  79. items = {row["ItemName"]: row for row in cursor.fetchall()}
  80. finally:
  81. connection.close()
  82. unit_configs = {}
  83. for unit in sorted(set(batch_to_unit.values())):
  84. unit_number = unit[0]
  85. oil = items.get(f"YSJ{unit_number}_5")
  86. coupling = items.get(f"YSJ{unit_number}_10")
  87. chain = items.get(f"YSJ{unit_number}_11")
  88. if not oil or not coupling or not chain:
  89. raise RuntimeError(f"site_point 缺少 {unit} 的 YSJ_5/10/11 配置")
  90. oil_entries = {
  91. "low": alarm_entry(oil, "PVLow"),
  92. "low_low": alarm_entry(oil, "PVLowLow"),
  93. "high": alarm_entry(oil, "PVHigh"),
  94. "high_high": alarm_entry(oil, "PVHighHigh"),
  95. }
  96. coupling_entries = {
  97. "high": alarm_entry(coupling, "PVHigh"),
  98. "high_high": alarm_entry(coupling, "PVHighHigh"),
  99. }
  100. chain_entries = {
  101. "high": alarm_entry(chain, "PVHigh"),
  102. "high_high": alarm_entry(chain, "PVHighHigh"),
  103. }
  104. oil_limits = {key: value for key, (value, _source) in oil_entries.items()}
  105. coupling_limits = {key: value for key, (value, _source) in coupling_entries.items()}
  106. chain_limits = {key: value for key, (value, _source) in chain_entries.items()}
  107. if any(value is None for value in oil_limits.values()):
  108. raise RuntimeError(f"{unit} YSJ_5 缺少完整低压报警阈值")
  109. if any(value is None for value in (*coupling_limits.values(), *chain_limits.values())):
  110. raise RuntimeError(f"{unit} YSJ_10/11 缺少完整振动报警阈值")
  111. unit_configs[unit] = {
  112. "unit": unit,
  113. "oil": oil_limits,
  114. "coupling": coupling_limits,
  115. "chain": chain_limits,
  116. "alarm_sources": {
  117. "oil": {key: source for key, (_value, source) in oil_entries.items()},
  118. "coupling": {key: source for key, (_value, source) in coupling_entries.items()},
  119. "chain": {key: source for key, (_value, source) in chain_entries.items()},
  120. },
  121. "metadata": {
  122. "oil_description": oil.get("ItemDescription") or "",
  123. "oil_units": oil.get("Units") or "",
  124. "coupling_description": coupling.get("ItemDescription") or "",
  125. "coupling_units": coupling.get("Units") or "",
  126. "chain_description": chain.get("ItemDescription") or "",
  127. "chain_units": chain.get("Units") or "",
  128. },
  129. }
  130. for batch, unit in batch_to_unit.items():
  131. result[batch] = {**unit_configs[unit], "batch": batch}
  132. return result
  133. __all__ = [
  134. "SITE_POINT_COLUMNS", "connect", "number", "load_pks_batches",
  135. "alarm_entry", "load_alarm_config",
  136. ]