test_alarm_flow.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. """同步映射 + 非法插接告警端到端测试
  2. 场景(PANEL_dtu_001_2, 地址=2):
  3. 硬件通信固定 6 字节(12 位)卡号;MQTT 侧 jumper_uid = 前 4 位 DTU 号 + 后 12 位卡号(16 位)。
  4. - 注入(模拟硬件读卡)用 12 位卡号;SYNC 下发与 MQTT 上行断言用 16 位。
  5. 1. 订阅 event / alarm / patchpanel status / response 主题
  6. 2. 清空注入 & 现有 port_state
  7. 3. SYNC_PORT_MAPPING: port 1 期望 0000AABBCCDDEEFF, port 5 期望 0000112233445566
  8. 4. 注入正确卡(port 1 = AABBCCDDEEFF, port 5 = 112233445566)-> 预期 CONNECT, 无告警
  9. 5. 注入错误卡(port 1 = DEADBEEF0001)-> 预期 MOVE + ILLEGAL_CONNECT
  10. 6. 注入空(port 5 拔出)-> 预期 DISCONNECT + ILLEGAL_DISCONNECT
  11. 7. 恢复正确卡(port 1 恢复 AABBCCDDEEFF)-> 预期 MOVE,告警清零
  12. 轮询间隔约 5s,脚本每步等待 8s 收 2 轮轮询结果。
  13. """
  14. import json
  15. import time
  16. import uuid
  17. import requests
  18. import paho.mqtt.client as mqtt
  19. DTU_HOST = "192.168.199.149"
  20. DTU_PORT = 5001
  21. BROKER = "xt.wenhq.top"
  22. BPORT = 8581
  23. BUSER, BPASS = "admin", "admin"
  24. PREFIX = "线架系统"
  25. CUSTOMER = "default_customer"
  26. DTU_ID = "dtu_001"
  27. PANEL_ID = "PANEL_dtu_001_2"
  28. DEVICE_ADDR = 2
  29. control_topic = f"{PREFIX}/{CUSTOMER}/dtu/{DTU_ID}/control"
  30. events, alarms, responses, statuses = [], [], [], []
  31. def ts():
  32. return time.strftime("%H:%M:%S")
  33. def on_connect(client, userdata, flags, rc):
  34. client.subscribe(f"{PREFIX}/{CUSTOMER}/#", qos=1)
  35. print(f"[{ts()}] 已订阅 {PREFIX}/{CUSTOMER}/#")
  36. def on_message(client, userdata, msg):
  37. try:
  38. env = json.loads(msg.payload.decode("utf-8"))
  39. pl = env.get("payload", {})
  40. except Exception:
  41. return
  42. if msg.topic.endswith("/event"):
  43. events.append(pl)
  44. print(f"[{ts()}] 🔔 EVENT panel={pl.get('panel_id')} port={pl.get('port_id')} "
  45. f"type={pl.get('event_type')} uid={pl.get('jumper_uid')} prev={pl.get('previous_jumper_uid')}")
  46. elif msg.topic.endswith("/alarm"):
  47. alarms.append(pl)
  48. print(f"[{ts()}] ⚠️ ALARM panel={pl.get('panel_id')} port={pl.get('port_id')} "
  49. f"type={pl.get('alarm_type')} sev={pl.get('severity')} "
  50. f"exp={pl.get('expected_jumper_uid')} act={pl.get('actual_jumper_uid')}")
  51. elif msg.topic.endswith("/response"):
  52. responses.append(pl)
  53. print(f"[{ts()}] ✅ RESP cmd={pl.get('command')} success={pl.get('success')} err={pl.get('error_code')}")
  54. elif "/patchpanel/" in msg.topic and msg.topic.endswith("/status"):
  55. if pl.get("panel_id") == PANEL_ID:
  56. connected = [p for p in pl.get("ports", []) if p.get("jumper_uid")]
  57. illegal = [p for p in pl.get("ports", []) if p.get("status") == "ILLEGAL"]
  58. statuses.append(pl)
  59. if connected or illegal:
  60. summary = ", ".join(f"p{p['port_id']}:{p['status']}={p.get('jumper_uid','-')}"
  61. for p in (connected + illegal))
  62. print(f"[{ts()}] 📊 面板状态: {summary}")
  63. def send_ctrl(client, command, target, params=None):
  64. env = {
  65. "msg_id": f"ctrl_{uuid.uuid4().hex[:8]}",
  66. "timestamp": int(time.time() * 1000),
  67. "dtu_id": DTU_ID, "type": "CONTROL",
  68. "payload": {"command": command, "target": target, "params": params or {}},
  69. }
  70. print(f"\n[{ts()}] ▶ 下发 {command} target={target} params={params}")
  71. client.publish(control_topic, json.dumps(env), qos=1)
  72. return env["msg_id"]
  73. def inject(cards_map):
  74. """cards_map: {port_id: hex12|None}, 全部注入到 DEVICE_ADDR"""
  75. body = {"cards": {str(DEVICE_ADDR): {str(k): v for k, v in cards_map.items()}}}
  76. r = requests.post(f"http://{DTU_HOST}:{DTU_PORT}/api/test/inject_cards",
  77. json=body, timeout=5)
  78. print(f"[{ts()}] 💉 注入 addr={DEVICE_ADDR} {cards_map} -> {r.status_code}")
  79. def clear_inject():
  80. r = requests.post(f"http://{DTU_HOST}:{DTU_PORT}/api/test/inject_cards",
  81. json={"cards": None}, timeout=5)
  82. print(f"[{ts()}] 🧹 清空注入 -> {r.status_code}")
  83. def wait(seconds, label=""):
  84. print(f"[{ts()}] ⏳ 等 {seconds}s {label}")
  85. time.sleep(seconds)
  86. def main():
  87. client = mqtt.Client(client_id="alarm_test")
  88. client.username_pw_set(BUSER, BPASS)
  89. client.on_connect = on_connect
  90. client.on_message = on_message
  91. client.connect(BROKER, BPORT, 60)
  92. client.loop_start()
  93. time.sleep(2)
  94. # 12 位硬件卡号(注入用,模拟硬件读卡)
  95. UID_A = "AABBCCDDEEFF"
  96. UID_B = "112233445566"
  97. UID_WRONG = "DEADBEEF0001"
  98. # SYNC 下发的 16 位 jumper_uid: 前 4 位 DTU 号 + 后 12 位卡号。
  99. # DTU 侧会剥掉前 4 位只用后 12 位比对,故前缀任意填(DTU 实际前缀由其 MAC 派生)。
  100. UID_A_MQTT = "0000" + UID_A
  101. UID_B_MQTT = "0000" + UID_B
  102. UID_WRONG_MQTT = "0000" + UID_WRONG
  103. # -------- 步骤 1: 清零 & 同步期望映射 --------
  104. print("\n===== 步骤 1: 同步期望映射(port1=A, port5=B)=====")
  105. events.clear(); alarms.clear(); responses.clear()
  106. send_ctrl(client, "SYNC_ALL_MAPPING", "all", {"mappings": [
  107. {"panel_id": PANEL_ID, "port_id": 1, "jumper_uid": UID_A_MQTT},
  108. {"panel_id": PANEL_ID, "port_id": 5, "jumper_uid": UID_B_MQTT},
  109. ]})
  110. wait(3, "等 SYNC 生效")
  111. # 清空所有假数据(无卡状态)
  112. inject({i: None for i in range(1, 25)})
  113. wait(6, "等一轮无卡轮询稳定 baseline")
  114. # -------- 步骤 2: 插入正确卡 --------
  115. print("\n===== 步骤 2: 插入正确卡(port1=A, port5=B)预期 CONNECT,无 ILLEGAL =====")
  116. events.clear(); alarms.clear()
  117. inject({1: UID_A, 5: UID_B, **{i: None for i in range(2, 25) if i != 5}})
  118. wait(8, "等 CONNECT 事件")
  119. connect_events = [e for e in events if e.get("event_type") == "CONNECT"]
  120. connect_ports = sorted(e.get("port_id") for e in connect_events)
  121. illegal_now = [a for a in alarms if a.get("alarm_type", "").startswith("ILLEGAL_")]
  122. print(f"[结果] CONNECT ports={connect_ports}, ILLEGAL 告警={len(illegal_now)}")
  123. assert 1 in connect_ports and 5 in connect_ports, "预期 port 1/5 CONNECT"
  124. assert len(illegal_now) == 0, f"不应有 ILLEGAL 告警: {illegal_now}"
  125. print("✅ 通过:正确卡不触发 ILLEGAL")
  126. # -------- 步骤 3: 换错卡 --------
  127. print("\n===== 步骤 3: port1 换成错卡(DEADBEEF0001)预期 MOVE + ILLEGAL_CONNECT =====")
  128. events.clear(); alarms.clear()
  129. inject({1: UID_WRONG, 5: UID_B, **{i: None for i in range(2, 25) if i != 5}})
  130. wait(8, "等 MOVE + ILLEGAL_CONNECT")
  131. move_events = [e for e in events if e.get("event_type") == "MOVE" and e.get("port_id") == 1]
  132. illegal_conn = [a for a in alarms if a.get("alarm_type") == "ILLEGAL_CONNECT" and a.get("port_id") == 1]
  133. print(f"[结果] port1 MOVE={len(move_events)}, ILLEGAL_CONNECT={len(illegal_conn)}")
  134. assert move_events, "预期 port1 MOVE 事件"
  135. assert illegal_conn, "预期 port1 ILLEGAL_CONNECT 告警"
  136. a0 = illegal_conn[0]
  137. # MQTT 上行 16 位 = DTU 前缀(由 MAC 派生) + 卡号(12 位);只比对后 12 位卡号
  138. assert a0.get("expected_jumper_uid", "").lower()[-12:] == UID_A.lower(), \
  139. f"expected_jumper_uid 错: {a0.get('expected_jumper_uid')}"
  140. assert a0.get("actual_jumper_uid", "").lower()[-12:] == UID_WRONG.lower(), \
  141. f"actual_jumper_uid 错: {a0.get('actual_jumper_uid')}"
  142. print(f"✅ 通过:ILLEGAL_CONNECT expected={a0.get('expected_jumper_uid')} actual={a0.get('actual_jumper_uid')} sev={a0.get('severity')}")
  143. # -------- 步骤 4: port5 拔出 --------
  144. print("\n===== 步骤 4: port5 拔出(保持 port1 错卡)预期 DISCONNECT + ILLEGAL_DISCONNECT =====")
  145. events.clear(); alarms.clear()
  146. inject({1: UID_WRONG, **{i: None for i in range(2, 25)}})
  147. wait(8, "等 DISCONNECT + ILLEGAL_DISCONNECT")
  148. disc_events = [e for e in events if e.get("event_type") == "DISCONNECT" and e.get("port_id") == 5]
  149. illegal_disc = [a for a in alarms if a.get("alarm_type") == "ILLEGAL_DISCONNECT" and a.get("port_id") == 5]
  150. print(f"[结果] port5 DISCONNECT={len(disc_events)}, ILLEGAL_DISCONNECT={len(illegal_disc)}")
  151. assert disc_events, "预期 port5 DISCONNECT 事件"
  152. assert illegal_disc, "预期 port5 ILLEGAL_DISCONNECT 告警"
  153. print(f"✅ 通过:ILLEGAL_DISCONNECT expected={illegal_disc[0].get('expected_jumper_uid')}")
  154. # -------- 步骤 5: 累计告警 3 次升级 CRITICAL --------
  155. print("\n===== 步骤 5: 累计告警观察 severity 升级(>=3 变 CRITICAL)=====")
  156. events.clear(); alarms.clear()
  157. inject({1: UID_WRONG, **{i: None for i in range(2, 25)}}) # 保持错卡+port5 拔出
  158. wait(18, "等 3 轮轮询累计告警")
  159. sev1 = [a.get("severity") for a in alarms if a.get("port_id") == 1]
  160. sev5 = [a.get("severity") for a in alarms if a.get("port_id") == 5]
  161. print(f"[结果] port1 severity 序列={sev1}, port5={sev5}")
  162. if any(s == "CRITICAL" for s in sev1 + sev5):
  163. print("✅ 通过:severity 已升级到 CRITICAL")
  164. else:
  165. print("ℹ 告警仍为 WARNING(未达 3 次阈值)")
  166. # -------- 步骤 6: 恢复正确卡 --------
  167. print("\n===== 步骤 6: 恢复正确卡 预期 MOVE + 告警清零 =====")
  168. events.clear(); alarms.clear()
  169. inject({1: UID_A, 5: UID_B, **{i: None for i in range(2, 25) if i != 5}})
  170. wait(8, "等 MOVE 恢复")
  171. move1 = [e for e in events if e.get("event_type") == "MOVE" and e.get("port_id") == 1]
  172. conn5 = [e for e in events if e.get("event_type") == "CONNECT" and e.get("port_id") == 5]
  173. illegal_still = [a for a in alarms if a.get("alarm_type", "").startswith("ILLEGAL_")]
  174. print(f"[结果] port1 MOVE={len(move1)}, port5 CONNECT={len(conn5)}, ILLEGAL={len(illegal_still)}")
  175. assert move1 or conn5, "预期 port1 恢复"
  176. print("✅ 通过:port 恢复到期望 UID,告警停止累计")
  177. # -------- 收尾: 清空注入 --------
  178. print("\n===== 收尾:清空注入 =====")
  179. clear_inject()
  180. print(f"\n===== 汇总:events={len(events)}, alarms={len(alarms)}, responses={len(responses)}, statuses={len(statuses)} =====")
  181. client.loop_stop()
  182. client.disconnect()
  183. if __name__ == "__main__":
  184. try:
  185. main()
  186. print("\n🎉 所有步骤 PASS")
  187. except AssertionError as e:
  188. print(f"\n❌ 断言失败: {e}")
  189. raise
  190. except Exception as e:
  191. print(f"\n❌ 异常: {e}")
  192. raise