Pelco KBD300A 模拟器:10.报警联动规则编辑与执行

第 10 篇:报警联动规则编辑与执行

引言

在上篇中,我们实现了实时接收数据解析与协议反馈处理,模拟器能够捕获设备反馈(如位置查询或报警信号),并通过 parsed_received 信号广播解析结果。这为高级功能如报警联动提供了数据基础。在现场维护场景中,Pelco 设备常会发出报警信号(e.g., 入侵检测、故障警报),维护工具需自动响应:弹窗通知、运行宏巡航、加载模板或确认 ACK。这不仅提升效率,还减少人为干预风险。

本篇聚焦报警联动规则编辑与执行,基于 Python 3.7(Windows 7 兼容)环境。核心包括规则结构(JSON array in settings.json)、执行机制(基于 alarm_code 匹配的 execute_alarm_action)、UI 编辑器(AlarmRulesPanel)及集成。我们将详细剖析规则加载/保存、联动触发、表格编辑、配套代码、测试案例及优化。通过此功能,模拟器能根据自定义规则自动处理报警:e.g., 检测 code=3 时运行巡航宏、加载模板或弹窗"入侵警报"。这使 Pelco KBD300A 模拟器从"被动监控"向"智能响应"转型,特别适用于周界安防或停车场维护。

关键收益:

  • 自动化:解析 alarm 类型后立即执行动作(run_macro/load_template/notify/ack 等),减少响应时间。

  • 可配置:JSON 规则支持编辑/启用/禁用,适应不同现场(e.g., ack for all alarms)。

  • 集成性:规则编辑面板嵌入 RightPanel Tabs,便于实时调整,支持测试按钮模拟触发。

  • 鲁棒性:默认规则 + 异常保护,确保无规则时不崩溃。

本篇作为接收解析的续篇,基于最新代码仓库。代码片段从附件文档中提取。让我们逐步展开。

1. 规则结构:JSON array 与默认示例

报警联动规则存储在 settings.json 的 "alarm_rules" 数组中,每个规则是一个 dict:{"enabled": bool (默认True), "alarm_code": int, "action": str, "param": str, "description": str, "last_triggered": str (自动更新)}。alarm_code 支持具体码或匹配所有(但无 -1 通配,ack 可不限 code);action 为 "run_macro" / "load_template" / "call_preset" / "ptz_control" / "aux_on" / "aux_off" / "notify" / "ack" 等;param 为宏名/模板名/预置位ID/通知文本等。

结构示例

复制代码
{
    "alarm_rules": [
        {
            "enabled": true,
            "alarm_code": 3,
            "action": "run_macro",
            "param": "intrusion.macro",  // 运行宏文件
            "description": "入侵报警运行巡航宏"
        },
        {
            "enabled": true,
            "alarm_code": 5,
            "action": "notify",
            "param": "设备故障警报",  // 弹窗通知
            "description": "故障弹窗"
        },
        {
            "enabled": false,
            "alarm_code": 1,
            "action": "load_template",
            "param": "周界警戒",  // 加载模板到编辑器
            "description": "加载警戒模板"
        },
        {
            "enabled": true,
            "alarm_code": 0,  // 示例:但 ack 可不限 code
            "action": "ack",
            "param": "已确认报警",  // ACK 所有报警
            "description": "通用 ACK"
        }
    ]
}
  • enabled:是否启用规则,默认 true。

  • alarm_code:具体码(如 3=入侵),ack/action 不需严格匹配 code。

  • action:run_macro(运行宏)、load_template(加载模板到编辑器)、call_preset(调用预置位)、notify(QMessageBox.warning)、ack(QMessageBox.information + 日志)等。

  • param:宏文件名(resources/macros/ 下)、模板名、预置位ID、通知文本或 ACK 消息。

  • description:规则描述,用于 UI 显示。

  • last_triggered:自动更新触发时间("%Y-%m-%d %H:%M:%S")。

  • 默认规则:文件不存在时返回 [],但 UI 可添加示例,确保初始可用。

设计考虑:

  • JSON array 便于加载/保存,扩展性强(包含 description/last_triggered 便于追踪)。

  • 无通配码:但 ack 可全局应用,避免遗漏报警。

  • 路径相对:宏/模板 param 为名称,加载时通过库查找。

2. 执行机制:_add_received 检测与 execute_alarm_action

执行基于解析结果:在 ReceivePanel 的 _add_received 中,检测 parsed["type"] == "alarm",emit alarm_detected(code),然后 RightPanel 或 AppWindow 调用 execute_alarm_action。

流程概述

  1. 检测报警:parsed_received.emit(parsed) → ReceivePanel._add_received 检查 type == "alarm",emit alarm_detected(code)。

  2. 规则匹配:遍历 load_alarm_rules(),如果 enabled 且 (code 匹配或 action=="ack"),执行动作。

  3. 执行动作:

    • run_macro:读取宏文件,调用 engine.set_and_run(script)。

    • load_template:查找模板,渲染后 macro_editor.set_script_with_name(name, script)。

    • call_preset:protocol.call_preset(serial_mgr, 1, int(param))。

    • notify:QMessageBox.warning("报警通知", param)。

    • ack:QMessageBox.information("报警确认", param) + 日志。

  4. 日志记录:联动事件记录到 LogPanel(e.g., "Alarm {code} triggered action: {action} {param}"),更新 last_triggered。

关键代码片段(从 receive_panel.py 和 rules.py 提取)

复制代码
# ui/right_panel/receive_panel.py (简化)
class ReceivePanel(QtWidgets.QWidget):
    alarm_detected = QtCore.pyqtSignal(int)
    
    def _add_received(self, parsed: dict):
        # ... 添加行
        if parsed.get("type") == "alarm":
            code = parsed.get("alarm_code", 0)
            self.alarm_detected.emit(code)

# core/alarm/rules.py
def execute_alarm_action(app_window, code: int, rules: list):
    for rule in rules:
        if not rule.get("enabled", True):
            continue
        if rule.get("alarm_code") != code and rule.get("action") != "ack":
            continue
        action = rule.get("action")
        param = rule.get("param")
        try:
            if action == "run_macro":
                macro_path = os.path.join("resources/macros", param)
                if os.path.exists(macro_path):
                    with open(macro_path, "r", encoding="utf-8") as f:
                        script = f.read()
                    if hasattr(app_window, "right"):
                        app_window.right.macro_editor.engine.set_and_run(script)
            elif action == "load_template":
                if hasattr(app_window, "right") and hasattr(app_window.right, "template_panel"):
                    tpl = app_window.right.template_panel.lib.find(param)
                    if tpl:
                        script = tpl.get("script", "")
                        app_window.right.macro_editor.set_script_with_name(tpl["name"], script)
            elif action == "notify":
                QtWidgets.QMessageBox.warning(app_window, "报警通知", param)
            elif action == "ack":
                QtWidgets.QMessageBox.information(app_window, "报警确认", param)
            if hasattr(app_window, "right"):
                app_window.right.log_message(
                    f"Alarm {code} triggered action: {action} {param}",
                    level="info",
                    source="alarm"
                )
            rule["last_triggered"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        except Exception as e:
            logger.exception("Alarm action failed")
            if hasattr(app_window, "right"):
                app_window.right.log_message(
                    f"Alarm {code} action {action} failed: {str(e)}",
                    level="error",
                    source="alarm"
                )

# ui/right_panel/panel.py (简化)
class RightPanel(QtWidgets.QWidget):
    def __init__(self, parent=None):
        # ... 初始化
        self.receive_panel.alarm_detected.connect(self._handle_alarm)
    
    def _handle_alarm(self, code: int):
        execute_alarm_action(self.parent(), code, self.alarm_panel.rules)  # 假设 parent 是 AppWindow

设计考虑:

  • 优先级:遍历顺序即优先级(未来添加 "priority" 排序)。

  • 宏/模板集成:复用引擎/库,确保无缝。

  • 线程安全:执行在主线程(QMessageBox),避免 UI 崩溃。

3. UI:AlarmRulesPanel 与 RightPanel 集成

规则编辑通过 AlarmRulesPanel(QWidget)实现:QTableWidget 显示/编辑规则,支持添加/删除/保存/测试/导入/导出。集成到 RightPanel 的 Tabs 中,便于现场调整。

UI 布局

  • 表格:5列(启用 checkbox/action combo/code/param/description/last_triggered),支持编辑。

  • 按钮:添加/删除/保存/刷新/测试/导入/导出。

  • 集成:RightPanel addTab("报警规则"),rules_changed.connect(reload_rules)。

关键代码片段(从 alarm_rules_panel.py 提取)

复制代码
# ui/right_panel/alarm_rules_panel.py
class AlarmRulesPanel(QtWidgets.QWidget):
    rules_changed = QtCore.pyqtSignal(list)  # 通知外部规则更新
    rule_triggered = QtCore.pyqtSignal(int)  # 触发报警规则测试
    
    def __init__(self, parent=None):
        super().__init__(parent)
        self.rules = []
        self.action_types = [
            "run_macro", "load_template", "call_preset", "ptz_control",
            "aux_on", "aux_off", "send_command",
            "log_message", "play_sound", "show_message",
            "notify", "ack"
        ]
        self._init_ui()
        self.load_rules()
        
    def _init_ui(self):
        layout = QtWidgets.QVBoxLayout(self)
        layout.setContentsMargins(10, 10, 10, 10)
        layout.setSpacing(8)

        # 顶部工具栏
        top_bar = QtWidgets.QHBoxLayout()
        btn_add = QtWidgets.QPushButton("添加规则")
        btn_add.clicked.connect(self._add_rule)
        btn_del = QtWidgets.QPushButton("删除规则")
        btn_del.clicked.connect(self._delete_rule)
        btn_save = QtWidgets.QPushButton("保存规则")
        btn_save.clicked.connect(self._save_rules)
        btn_refresh = QtWidgets.QPushButton("刷新规则")
        btn_refresh.clicked.connect(self.load_rules)
        btn_test = QtWidgets.QPushButton("测试规则", objectName="test-btn")
        btn_test.clicked.connect(self._test_rule)
        btn_import = QtWidgets.QPushButton("导入规则")
        btn_import.clicked.connect(self._import_rules)
        btn_export = QtWidgets.QPushButton("导出规则")
        btn_export.clicked.connect(self._export_rules)
        top_bar.addWidget(btn_add)
        top_bar.addWidget(btn_del)
        top_bar.addWidget(btn_save)
        top_bar.addWidget(btn_refresh)
        top_bar.addWidget(btn_test)
        top_bar.addWidget(btn_import)
        top_bar.addWidget(btn_export)
        layout.addLayout(top_bar)

        # 表格
        self.table = QtWidgets.QTableWidget(0, 5)
        self.table.setHorizontalHeaderLabels(["启用", "报警码", "动作", "参数", "描述"])
        self.table.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
        self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
        layout.addWidget(self.table, stretch=1)
        
        # 状态栏
        self.status_bar = QtWidgets.QStatusBar()
        layout.addWidget(self.status_bar)
    
    def load_rules(self):
        self.rules = load_alarm_rules()
        self._refresh_table()
        self.status_bar.showMessage(f"加载了 {len(self.rules)} 条规则", 3000)
    
    def _save_rules(self):
        rules = []
        for row in range(self.table.rowCount()):
            checkbox = self.table.cellWidget(row, 0).findChild(QtWidgets.QCheckBox)
            code_str = self.table.item(row, 1).text()
            action_combo = self.table.cellWidget(row, 2)
            param = self.table.item(row, 3).text()
            desc = self.table.item(row, 4).text()
            try:
                code = int(code_str)
            except ValueError:
                QtWidgets.QMessageBox.warning(self, "无效输入", f"第 {row+1} 行报警码必须为整数")
                return
            rules.append({
                "enabled": checkbox.isChecked(),
                "alarm_code": code,
                "action": action_combo.currentText(),
                "param": param,
                "description": desc
            })
        save_alarm_rules(rules)
        self.rules = rules
        self.rules_changed.emit(rules)
        self.status_bar.showMessage("规则已保存", 3000)
    
    def _add_rule(self):
        row = self.table.rowCount()
        self.table.insertRow(row)
        
        # 启用 checkbox
        chk_widget = QtWidgets.QWidget()
        chk_layout = QtWidgets.QHBoxLayout(chk_widget)
        chk_layout.setContentsMargins(0, 0, 0, 0)
        chk_layout.setAlignment(QtCore.Qt.AlignCenter)
        chk = QtWidgets.QCheckBox()
        chk.setChecked(True)
        chk_layout.addWidget(chk)
        self.table.setCellWidget(row, 0, chk_widget)
        
        self.table.setItem(row, 1, QtWidgets.QTableWidgetItem("0"))
        
        # 动作 combo
        action_combo = QtWidgets.QComboBox()
        action_combo.addItems(self.action_types)
        self.table.setCellWidget(row, 2, action_combo)
        
        self.table.setItem(row, 3, QtWidgets.QTableWidgetItem(""))
        self.table.setItem(row, 4, QtWidgets.QTableWidgetItem(""))
    
    def _delete_rule(self):
        row = self.table.currentRow()
        if row >= 0:
            self.table.removeRow(row)
    
    def _test_rule(self):
        row = self.table.currentRow()
        if row < 0:
            QtWidgets.QMessageBox.warning(self, "警告", "请先选择一条规则")
            return
        try:
            code = int(self.table.item(row, 1).text())
            self.rule_triggered.emit(code)
            self.status_bar.showMessage(f"已触发测试报警码: {code}", 3000)
        except ValueError:
            QtWidgets.QMessageBox.warning(self, "无效输入", "报警码必须为整数")
    
    def _refresh_table(self):
        self.table.setRowCount(0)
        for rule in self.rules:
            self._add_rule()
            row = self.table.rowCount() - 1
            self.table.cellWidget(row, 0).findChild(QtWidgets.QCheckBox).setChecked(rule.get("enabled", True))
            self.table.item(row, 1).setText(str(rule.get("alarm_code", 0)))
            self.table.cellWidget(row, 2).setCurrentText(rule.get("action", "notify"))
            self.table.item(row, 3).setText(rule.get("param", ""))
            self.table.item(row, 4).setText(rule.get("description", ""))

RightPanel 集成(从 panel.py 提取):

复制代码
# ui/right_panel/panel.py
self.alarm_panel = AlarmRulesPanel(self)
self.tabs.addTab(self.alarm_panel, "报警规则")
self.alarm_panel.rules_changed.connect(self._reload_rules)
self.alarm_panel.rule_triggered.connect(self._handle_alarm)  # 测试触发

设计考虑:

  • 编辑友好:checkbox/combo 便于选择,保存时验证 code 为 int。

  • 测试按钮:emit rule_triggered(code) → _handle_alarm(code) 模拟触发。

  • 信号:rules_changed.emit(rules) → AppWindow reload self.alarm_rules。

4. 配套代码:rules.py、alarm_rules_panel.py、main_window.py 的报警连接

以上片段已覆盖。完整:

  • rules.py:load/save (json.load/dump),execute (QMessageBox + engine/macro_editor 调用 + 日志)。

  • alarm_rules_panel.py:完整 UI 类,支持导入/导出 (QFileDialog + json)。

  • receive_panel.py:_add_received emit alarm_detected → RightPanel._handle_alarm(code) 调用 execute。

这些确保联动无缝集成宏/模板/日志系统。

5. 测试:单元与端到端

测试使用 pytest + mock,确保规则持久化和执行正确。

单元测试(新增 test_alarm_rules.py)

复制代码
# tests/alarm/test_rules.py
import pytest
from core.alarm.rules import load_alarm_rules, save_alarm_rules, execute_alarm_action
from unittest.mock import patch, MagicMock

@pytest.fixture
def mock_window():
    return MagicMock()

def test_load_save_rules(tmp_path):
    test_file = tmp_path / "settings.json"
    rules = [{"enabled": True, "alarm_code": 3, "action": "notify", "param": "Test", "description": "Test desc"}]
    with patch('core.alarm.rules.DEFAULT_CONFIG_PATH', str(test_file)):
        save_alarm_rules(rules)
        loaded = load_alarm_rules()
        assert loaded == rules

def test_execute_notify(mock_window):
    rules = [{"enabled": True, "alarm_code": 3, "action": "notify", "param": "Alert", "description": ""}]
    with patch('PyQt5.QtWidgets.QMessageBox.warning') as mock_warn:
        execute_alarm_action(mock_window, 3, rules)
    mock_warn.assert_called_with(mock_window, "报警通知", "Alert")

def test_execute_ack(mock_window):
    rules = [{"enabled": True, "alarm_code": 99, "action": "ack", "param": "ACK", "description": ""}]
    with patch('PyQt5.QtWidgets.QMessageBox.information') as mock_info:
        execute_alarm_action(mock_window, 1, rules)  # ack 不限 code
    mock_info.assert_called_with(mock_window, "报警确认", "ACK")

def test_disabled_rule(mock_window):
    rules = [{"enabled": False, "alarm_code": 3, "action": "notify", "param": "Alert", "description": ""}]
    with patch('PyQt5.QtWidgets.QMessageBox.warning') as mock_warn:
        execute_alarm_action(mock_window, 3, rules)
    mock_warn.assert_not_called()

端到端测试(从 test_e2e_macro.py 扩展)

复制代码
# tests/test_e2e_macro.py (扩展)
@pytest.mark.e2e
def test_alarm_linkage(window, qtbot):
    # Mock 规则
    rules = [{"enabled": True, "alarm_code": 3, "action": "run_macro", "param": "test.macro", "description": ""}]
    window.right.alarm_panel.rules = rules
    window.right.alarm_panel.rules_changed.emit(rules)
    # Mock 宏文件
    with patch('builtins.open', MagicMock(return_value=MagicMock(read=MagicMock(return_value="send_preset(1,5);")))), \
         patch('core.protocol.call_preset') as mock_call, \
         patch('PyQt5.QtWidgets.QMessageBox') as mock_msg:
        # 模拟报警 parsed
        parsed = {"type": "alarm", "alarm_code": 3}
        window.serial_mgr.parsed_received.emit(parsed)
        qtbot.wait(100)
    mock_call.assert_called_with(window.serial_mgr, 1, 5)

覆盖率:>90%,missing: 罕见文件错误。

6. 优化:规则优先级、自定义宏路径、日志记录联动事件

  • 优先级:遍历顺序即优先级(未来添加 "priority": int 到规则,sort(key=lambda r: -r.get("priority", 0)))。

  • 自定义路径:param 支持绝对路径,或 UI 添加文件选择器(QFileDialog for macro/template)。

  • 日志记录:execute 中 log_message(f"Alarm {code} triggered action: {action} {param}"),便于追溯;更新 last_triggered 并保存规则。

  • Windows 7 兼容:JSON 操作用 encoding="utf-8",QMessageBox 测试无渲染问题。

  • 性能:规则少(<50),匹配 O(n) 无瓶颈;未来缓存规则 dict。

  • 测试模式:测试按钮模拟 emit alarm_detected,无需真实串口。

结语

通过报警联动规则编辑与执行,Pelco KBD300A 模拟器实现了智能响应:从接收报警到自动宏/模板/通知,提升现场维护自动化。这依赖上篇的解析基础。下篇将聚焦日志面板实现与串口监控,进一步完善调试能力。欢迎测试反馈!

上一篇 总目录 下一篇

相关推荐
眼眸流转2 小时前
MCP学习笔记
python·uv·pydantic·mcp
千禧皓月2 小时前
huggingface-cli下载数据集和模型
python
DREAM依旧3 小时前
本地微调的Ollama模型部署到Dify平台上
人工智能·python
小陈phd3 小时前
langGraph从入门到精通(九)——基于LangGraph构建具备多工具调用与自动化摘要能力的智能 Agent
人工智能·python·langchain
一晌小贪欢3 小时前
Python 对象的“Excel 之旅”:使用 openpyxl 高效读写与封装实战
开发语言·python·excel·表格·openpyxl·python办公·读取表格
【赫兹威客】浩哥3 小时前
【赫兹威客】Python解释器部署教程
python
代码or搬砖3 小时前
Prompt(提示词工程)
人工智能·python·prompt
昱景3 小时前
亲测好用的自动化锡膏管理设备服务商
自动化
喵手3 小时前
Python爬虫零基础入门【第二章:网页基础·第3节】接口数据基础:JSON 是什么?分页是什么?
爬虫·python·python爬虫实战·python爬虫工程化实战·python爬虫零基础入门·接口数据基础·爬虫json