devices.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. """Device-code resolution for PKS scans.
  2. ``--device`` accepts compressor codes such as ``7#`` / ``8#`` / ``9#`` (also
  3. tolerates ``7`` or ``7号机``). A device maps to one or more PKS import batches
  4. through ``compressor_data_import.directory_path``; the batch ids are then used
  5. for the (index-friendly) hourly aggregation so results stay identical to the
  6. original batch-based scanner output.
  7. 4#/5#/6# exist in the database but are intentionally not part of the default
  8. scan set.
  9. """
  10. from __future__ import annotations
  11. import re
  12. from datetime import datetime, timedelta
  13. from .db import connect
  14. DEVICE_CODES = ("7#", "8#", "9#")
  15. DEVICE_TO_UNIT = {"7#": "7号机", "8#": "8号机", "9#": "9号机"}
  16. def normalize_device(value) -> str:
  17. text = str(value).strip()
  18. if text in DEVICE_TO_UNIT:
  19. return text
  20. digits = re.sub(r"\D", "", text)
  21. candidate = f"{digits}#" if digits else ""
  22. if candidate in DEVICE_TO_UNIT:
  23. return candidate
  24. raise ValueError(f"无法识别的压缩机编号:{value!r}(应为 7# / 8# / 9#)")
  25. def normalize_devices(values=None) -> list[str]:
  26. devices = []
  27. for value in (values or DEVICE_CODES):
  28. device = normalize_device(value)
  29. if device not in devices:
  30. devices.append(device)
  31. return devices
  32. def load_device_batches(devices=None) -> dict[str, list[int]]:
  33. """Resolve ``device -> [import_batch_id, ...]`` from the import metadata."""
  34. selected = normalize_devices(devices)
  35. placeholders = ",".join(["%s"] * len(selected))
  36. connection = connect()
  37. try:
  38. with connection.cursor() as cursor:
  39. cursor.execute(
  40. f"""
  41. SELECT id, directory_path
  42. FROM compressor_data_import
  43. WHERE data_type = 'PKS'
  44. AND directory_path IN ({placeholders})
  45. ORDER BY id
  46. """,
  47. selected,
  48. )
  49. result: dict[str, list[int]] = {device: [] for device in selected}
  50. for row in cursor.fetchall():
  51. device = str(row["directory_path"]).strip()
  52. if device in result:
  53. result[device].append(int(row["id"]))
  54. finally:
  55. connection.close()
  56. missing = [device for device, batches in result.items() if not batches]
  57. if missing:
  58. raise RuntimeError(f"以下设备没有找到 PKS 导入批次:{missing}")
  59. return result
  60. def batch_to_device(device_batches: dict[str, list[int]]) -> dict[int, str]:
  61. return {batch: device for device, batches in device_batches.items() for batch in batches}
  62. def batch_to_unit(device_batches: dict[str, list[int]]) -> dict[int, str]:
  63. return {batch: DEVICE_TO_UNIT[device] for device, batches in device_batches.items() for batch in batches}
  64. def floor_hour(moment: datetime) -> datetime:
  65. return moment.replace(minute=0, second=0, microsecond=0)
  66. def device_data_range(device_batches: dict[str, list[int]]) -> dict[str, tuple[datetime, datetime]]:
  67. """Return ``device -> (min_sample_time, max_sample_time)`` using the batch index."""
  68. ranges: dict[str, tuple[datetime, datetime]] = {}
  69. connection = connect()
  70. try:
  71. with connection.cursor() as cursor:
  72. for device, batches in device_batches.items():
  73. placeholders = ",".join(["%s"] * len(batches))
  74. cursor.execute(
  75. f"""
  76. SELECT MIN(sample_time) AS mn, MAX(sample_time) AS mx
  77. FROM pks_long_sample
  78. WHERE import_batch_id IN ({placeholders})
  79. """,
  80. batches,
  81. )
  82. row = cursor.fetchone()
  83. if row and row["mn"] and row["mx"]:
  84. ranges[device] = (row["mn"], row["mx"])
  85. finally:
  86. connection.close()
  87. if not ranges:
  88. raise RuntimeError("所选设备没有可用样本时间范围")
  89. return ranges
  90. def auto_range(device_batches: dict[str, list[int]]) -> tuple[datetime, datetime]:
  91. """Settled scan range: floor(min) .. floor(max)+1h (last complete hour included)."""
  92. ranges = device_data_range(device_batches)
  93. start = min(floor_hour(mn) for mn, _ in ranges.values())
  94. end = max(floor_hour(mx) + timedelta(hours=1) for _, mx in ranges.values())
  95. return start, end
  96. __all__ = [
  97. "DEVICE_CODES", "DEVICE_TO_UNIT", "normalize_device", "normalize_devices",
  98. "load_device_batches", "batch_to_device", "batch_to_unit", "floor_hour",
  99. "device_data_range", "auto_range",
  100. ]