realtime_service.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. #!/usr/bin/env python3
  2. """Realtime PKS alarm scanner.
  3. Runs the same pressure/vibration algorithm as the full scan over a rolling
  4. window ending at the current (settled) hour, then merges the resulting stage
  5. segments into ``compressor_alarm``. A growing same-level segment is merged into
  6. one row (never split), while different levels remain separate rows.
  7. Modes
  8. -----
  9. * ``--once`` run a single tick at the current local time.
  10. * ``--loop --interval 300`` run a tick every ``interval`` seconds (CLI timer).
  11. * ``--simulate "t1,t2,t3"`` smoke test: run ticks at the given times and
  12. (by default) roll the database back afterwards.
  13. Examples
  14. --------
  15. python models/realtime_service.py --once
  16. python models/realtime_service.py --loop --interval 300
  17. python models/realtime_service.py --simulate "2026-02-03 20:00:00,2026-02-04 00:00:00"
  18. """
  19. from __future__ import annotations
  20. import argparse
  21. import sys
  22. import time
  23. from collections import Counter
  24. from datetime import datetime, timedelta
  25. from pathlib import Path
  26. sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
  27. from models.common import alarm_store, db, devices, io, model_predict, queries
  28. from models.generalized import pressure_algorithm
  29. from models.vibration import vibration_algorithm
  30. REALTIME_CHUNK_DAYS = 7
  31. def run_tick(now: datetime, device_list: list[str], lookback_days: int, write: bool, journal: list | None, with_model: bool = False) -> dict:
  32. device_batches = devices.load_device_batches(device_list)
  33. unit_by_batch = devices.batch_to_unit(device_batches)
  34. batch_device = devices.batch_to_device(device_batches)
  35. configs = db.load_alarm_config(unit_by_batch)
  36. ranges = devices.device_data_range(device_batches)
  37. latest = max(mx for _, mx in ranges.values())
  38. end = min(devices.floor_hour(now), devices.floor_hour(latest) + timedelta(hours=1))
  39. start = end - timedelta(days=lookback_days)
  40. if start >= end:
  41. return {"tick": now, "end": end, "action": "no_data", "inserted": 0, "updated": 0}
  42. rows = queries.fetch_all_hourly(start, end, REALTIME_CHUNK_DAYS, unit_by_batch, configs, sorted(unit_by_batch))
  43. hourly_levels, hour_segments, cycles = pressure_algorithm.run(rows, configs)
  44. events = vibration_algorithm.run(rows, configs)
  45. if with_model:
  46. model_predict.annotate("pressure", rows, hour_segments)
  47. model_predict.annotate("vibration", rows, events)
  48. alarms = alarm_store.build_alarms(io.clean_rows(hour_segments), io.clean_rows(events), batch_device)
  49. result = alarm_store.merge_realtime(alarms, start, end, journal=journal if write else None, dry_run=not write)
  50. return {
  51. "tick": now, "window": (start, end), "hourly_rows": len(rows),
  52. "pressure_segments": len(hour_segments), "vibration_events": len(events),
  53. "segments_to_store": len(alarms), **result,
  54. }
  55. def format_tick(summary: dict) -> str:
  56. window = summary.get("window")
  57. window_text = f"{window[0]}..{window[1]}" if window else "-"
  58. return (
  59. f"[{summary['tick']}] window={window_text} 压力段={summary.get('pressure_segments')} "
  60. f"振动段={summary.get('vibration_events')} 写入段={summary.get('segments_to_store')} "
  61. f"insert={summary.get('inserted')} update={summary.get('updated')} merged_extra={summary.get('merged_extra')}"
  62. )
  63. def snapshot(device_list: list[str], limit: int = 8) -> str:
  64. rows = alarm_store.fetch_alarms(device_list)
  65. counter = Counter((r["device_code"], r["alarm_type"], r["device_point"], r["alarm_level"]) for r in rows)
  66. return "; ".join(f"{k}={v}" for k, v in sorted(counter.items(), key=lambda item: str(item[0]))[:limit])
  67. def main() -> None:
  68. parser = argparse.ArgumentParser(description="PKS 实时告警扫描(结果写入 compressor_alarm)")
  69. parser.add_argument("--device", nargs="+", default=list(devices.DEVICE_CODES))
  70. parser.add_argument("--lookback-days", type=int, default=45)
  71. parser.add_argument("--once", action="store_true", help="运行一次(默认)")
  72. parser.add_argument("--loop", action="store_true", help="按 --interval 循环运行")
  73. parser.add_argument("--interval", type=int, default=300, help="循环间隔秒,默认 300")
  74. parser.add_argument("--simulate", default=None, help="逗号分隔的时间列表,冒烟测试")
  75. parser.add_argument("--with-model", action="store_true", help="对故障段做模型滑动预测(默认关闭)")
  76. parser.add_argument("--no-rollback", action="store_true", help="冒烟测试后不回滚数据库")
  77. args = parser.parse_args()
  78. device_list = devices.normalize_devices(args.device)
  79. if args.lookback_days < 1:
  80. parser.error("--lookback-days 必须大于 0")
  81. if args.simulate:
  82. times = [queries.parse_dt(item.strip()) for item in args.simulate.split(",") if item.strip()]
  83. journal: list = []
  84. for moment in times:
  85. summary = run_tick(moment, device_list, args.lookback_days, write=True, journal=journal, with_model=args.with_model)
  86. print(format_tick(summary), flush=True)
  87. print(" 当前告警分布:" + snapshot(device_list), flush=True)
  88. if not args.no_rollback:
  89. undone = alarm_store.rollback(journal)
  90. print(f"冒烟测试回滚:{undone}", flush=True)
  91. return
  92. if args.loop:
  93. print(f"实时服务启动,每 {args.interval} 秒运行一次;Ctrl+C 退出。", flush=True)
  94. while True:
  95. summary = run_tick(datetime.now(), device_list, args.lookback_days, write=True, journal=None, with_model=args.with_model)
  96. print(format_tick(summary), flush=True)
  97. time.sleep(args.interval)
  98. summary = run_tick(datetime.now(), device_list, args.lookback_days, write=True, journal=None, with_model=args.with_model)
  99. print(format_tick(summary))
  100. print("当前告警分布:" + snapshot(device_list))
  101. if __name__ == "__main__":
  102. main()