| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124 |
- #!/usr/bin/env python3
- """Realtime PKS alarm scanner.
- Runs the same pressure/vibration algorithm as the full scan over a rolling
- window ending at the current (settled) hour, then merges the resulting stage
- segments into ``compressor_alarm``. A growing same-level segment is merged into
- one row (never split), while different levels remain separate rows.
- Modes
- -----
- * ``--once`` run a single tick at the current local time.
- * ``--loop --interval 300`` run a tick every ``interval`` seconds (CLI timer).
- * ``--simulate "t1,t2,t3"`` smoke test: run ticks at the given times and
- (by default) roll the database back afterwards.
- Examples
- --------
- python models/realtime_service.py --once
- python models/realtime_service.py --loop --interval 300
- python models/realtime_service.py --simulate "2026-02-03 20:00:00,2026-02-04 00:00:00"
- """
- from __future__ import annotations
- import argparse
- import sys
- import time
- from collections import Counter
- from datetime import datetime, timedelta
- from pathlib import Path
- sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
- from models.common import alarm_store, db, devices, io, model_predict, queries
- from models.generalized import pressure_algorithm
- from models.vibration import vibration_algorithm
- REALTIME_CHUNK_DAYS = 7
- def run_tick(now: datetime, device_list: list[str], lookback_days: int, write: bool, journal: list | None, with_model: bool = False) -> dict:
- device_batches = devices.load_device_batches(device_list)
- unit_by_batch = devices.batch_to_unit(device_batches)
- batch_device = devices.batch_to_device(device_batches)
- configs = db.load_alarm_config(unit_by_batch)
- ranges = devices.device_data_range(device_batches)
- latest = max(mx for _, mx in ranges.values())
- end = min(devices.floor_hour(now), devices.floor_hour(latest) + timedelta(hours=1))
- start = end - timedelta(days=lookback_days)
- if start >= end:
- return {"tick": now, "end": end, "action": "no_data", "inserted": 0, "updated": 0}
- rows = queries.fetch_all_hourly(start, end, REALTIME_CHUNK_DAYS, unit_by_batch, configs, sorted(unit_by_batch))
- hourly_levels, hour_segments, cycles = pressure_algorithm.run(rows, configs)
- events = vibration_algorithm.run(rows, configs)
- if with_model:
- model_predict.annotate("pressure", rows, hour_segments)
- model_predict.annotate("vibration", rows, events)
- alarms = alarm_store.build_alarms(io.clean_rows(hour_segments), io.clean_rows(events), batch_device)
- result = alarm_store.merge_realtime(alarms, start, end, journal=journal if write else None, dry_run=not write)
- return {
- "tick": now, "window": (start, end), "hourly_rows": len(rows),
- "pressure_segments": len(hour_segments), "vibration_events": len(events),
- "segments_to_store": len(alarms), **result,
- }
- def format_tick(summary: dict) -> str:
- window = summary.get("window")
- window_text = f"{window[0]}..{window[1]}" if window else "-"
- return (
- f"[{summary['tick']}] window={window_text} 压力段={summary.get('pressure_segments')} "
- f"振动段={summary.get('vibration_events')} 写入段={summary.get('segments_to_store')} "
- f"insert={summary.get('inserted')} update={summary.get('updated')} merged_extra={summary.get('merged_extra')}"
- )
- def snapshot(device_list: list[str], limit: int = 8) -> str:
- rows = alarm_store.fetch_alarms(device_list)
- counter = Counter((r["device_code"], r["alarm_type"], r["device_point"], r["alarm_level"]) for r in rows)
- return "; ".join(f"{k}={v}" for k, v in sorted(counter.items(), key=lambda item: str(item[0]))[:limit])
- def main() -> None:
- parser = argparse.ArgumentParser(description="PKS 实时告警扫描(结果写入 compressor_alarm)")
- parser.add_argument("--device", nargs="+", default=list(devices.DEVICE_CODES))
- parser.add_argument("--lookback-days", type=int, default=45)
- parser.add_argument("--once", action="store_true", help="运行一次(默认)")
- parser.add_argument("--loop", action="store_true", help="按 --interval 循环运行")
- parser.add_argument("--interval", type=int, default=300, help="循环间隔秒,默认 300")
- parser.add_argument("--simulate", default=None, help="逗号分隔的时间列表,冒烟测试")
- parser.add_argument("--with-model", action="store_true", help="对故障段做模型滑动预测(默认关闭)")
- parser.add_argument("--no-rollback", action="store_true", help="冒烟测试后不回滚数据库")
- args = parser.parse_args()
- device_list = devices.normalize_devices(args.device)
- if args.lookback_days < 1:
- parser.error("--lookback-days 必须大于 0")
- if args.simulate:
- times = [queries.parse_dt(item.strip()) for item in args.simulate.split(",") if item.strip()]
- journal: list = []
- for moment in times:
- summary = run_tick(moment, device_list, args.lookback_days, write=True, journal=journal, with_model=args.with_model)
- print(format_tick(summary), flush=True)
- print(" 当前告警分布:" + snapshot(device_list), flush=True)
- if not args.no_rollback:
- undone = alarm_store.rollback(journal)
- print(f"冒烟测试回滚:{undone}", flush=True)
- return
- if args.loop:
- print(f"实时服务启动,每 {args.interval} 秒运行一次;Ctrl+C 退出。", flush=True)
- while True:
- summary = run_tick(datetime.now(), device_list, args.lookback_days, write=True, journal=None, with_model=args.with_model)
- print(format_tick(summary), flush=True)
- time.sleep(args.interval)
- summary = run_tick(datetime.now(), device_list, args.lookback_days, write=True, journal=None, with_model=args.with_model)
- print(format_tick(summary))
- print("当前告警分布:" + snapshot(device_list))
- if __name__ == "__main__":
- main()
|