2026-05-18 09:12:14 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-05-18 12:35:27 +08:00
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
from typing import Any, Dict, List, Optional
|
2026-05-18 09:12:14 +08:00
|
|
|
|
|
|
|
|
|
|
from app.adapters.device_client import CDeviceClient, MockDeviceClient
|
|
|
|
|
|
from app.cache.memory_store import memory_store
|
|
|
|
|
|
from app.core.config import settings
|
|
|
|
|
|
from app.core.security import hash_password
|
|
|
|
|
|
from app.repositories.alarm_repo import AlarmRepository
|
|
|
|
|
|
from app.repositories.json_config_repo import JsonConfigRepository
|
|
|
|
|
|
from app.schemas.platform import (
|
2026-06-29 08:45:20 +08:00
|
|
|
|
AlarmEvent,
|
2026-05-18 09:12:14 +08:00
|
|
|
|
AiAlarmSettingIn,
|
|
|
|
|
|
ChannelConfigIn,
|
|
|
|
|
|
DeviceConfigIn,
|
2026-05-19 14:47:08 +08:00
|
|
|
|
DevicePasswordUpdateIn,
|
2026-05-18 09:12:14 +08:00
|
|
|
|
DeviceStatus,
|
|
|
|
|
|
LineAlarmSettingIn,
|
2026-05-19 09:26:51 +08:00
|
|
|
|
NetConfigItem,
|
2026-05-19 14:47:08 +08:00
|
|
|
|
LineData,
|
2026-05-18 09:12:14 +08:00
|
|
|
|
RealtimeData,
|
|
|
|
|
|
SwitchControlIn,
|
|
|
|
|
|
SystemConfigIn,
|
2026-06-26 16:36:02 +08:00
|
|
|
|
TimeSyncConfigIn,
|
|
|
|
|
|
ValueGroup,
|
2026-05-19 09:26:51 +08:00
|
|
|
|
UartConfigItem,
|
2026-05-18 09:12:14 +08:00
|
|
|
|
)
|
|
|
|
|
|
from app.ws.manager import ws_manager
|
|
|
|
|
|
|
2026-06-29 08:45:20 +08:00
|
|
|
|
# 主站侧操作类事件代码
|
|
|
|
|
|
OPERATION_EVENT_CODE = {
|
|
|
|
|
|
1: "控制分/合",
|
|
|
|
|
|
2: "读定值失败",
|
|
|
|
|
|
3: "线路定值修改",
|
|
|
|
|
|
4: "AI报警参数修改",
|
|
|
|
|
|
5: "AI通道参数修改",
|
|
|
|
|
|
6: "AO通道参数修改",
|
|
|
|
|
|
7: "通讯参数修改",
|
|
|
|
|
|
8: "密码修改",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 操作类事件 event_type
|
|
|
|
|
|
EVENT_TYPE_OPERATION = 4
|
|
|
|
|
|
|
|
|
|
|
|
# 开机自检异常归入报警类,避免前端事件报表出现未知类型
|
|
|
|
|
|
EVENT_TYPE_SELF_CHECK = 3
|
|
|
|
|
|
SELF_CHECK_COMMUNICATION_ERROR_CODE = 99
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _generate_operation_event_content(event_code: int, line_code: int = 0, detail: str = "") -> str:
|
|
|
|
|
|
"""根据操作事件代码生成事件内容描述文字。
|
|
|
|
|
|
|
|
|
|
|
|
参数
|
|
|
|
|
|
----
|
|
|
|
|
|
event_code : int
|
|
|
|
|
|
操作事件代码 (1~8)
|
|
|
|
|
|
line_code : int
|
|
|
|
|
|
关联线路编号,0 表示装置级
|
|
|
|
|
|
detail : str
|
|
|
|
|
|
附加描述信息,如网卡名、端口号等
|
|
|
|
|
|
"""
|
|
|
|
|
|
label = OPERATION_EVENT_CODE.get(event_code, f"未知操作事件(code={event_code})")
|
|
|
|
|
|
parts = [label]
|
|
|
|
|
|
if line_code > 0:
|
|
|
|
|
|
parts.append(f"线路{line_code}")
|
|
|
|
|
|
if detail:
|
|
|
|
|
|
parts.append(detail)
|
|
|
|
|
|
return ";".join(parts)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _collect_startup_status_abnormalities(status: DeviceStatus) -> List[str]:
|
|
|
|
|
|
abnormal_items: List[str] = []
|
|
|
|
|
|
if status.self_check != "正常":
|
|
|
|
|
|
abnormal_items.append(f"装置自检={status.self_check}")
|
|
|
|
|
|
for label, value in (
|
|
|
|
|
|
("网口1", status.net1),
|
|
|
|
|
|
("网口2", status.net2),
|
|
|
|
|
|
("网口3", status.net3),
|
|
|
|
|
|
("网口4", status.net4),
|
|
|
|
|
|
("串口1", status.uart1),
|
|
|
|
|
|
("串口2", status.uart2),
|
|
|
|
|
|
("串口3", status.uart3),
|
|
|
|
|
|
("串口4", status.uart4),
|
|
|
|
|
|
):
|
|
|
|
|
|
if value != "正常":
|
|
|
|
|
|
abnormal_items.append(f"{label}={value}")
|
|
|
|
|
|
return abnormal_items
|
|
|
|
|
|
|
2026-05-18 09:12:14 +08:00
|
|
|
|
|
|
|
|
|
|
class PlatformService:
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
|
self.config_repo = JsonConfigRepository()
|
|
|
|
|
|
self.alarm_repo = AlarmRepository()
|
|
|
|
|
|
self.device_client = MockDeviceClient() if settings.use_mock_device else CDeviceClient()
|
|
|
|
|
|
|
2026-05-19 14:47:08 +08:00
|
|
|
|
def _safe_ratio(self, primary: float, secondary: float, default: float = 1.0) -> float:
|
2026-06-12 13:32:58 +08:00
|
|
|
|
"""安全计算变比。
|
|
|
|
|
|
|
|
|
|
|
|
secondary 接近 0 时返回默认值,避免除 0。
|
|
|
|
|
|
"""
|
2026-05-19 14:47:08 +08:00
|
|
|
|
if abs(float(secondary)) < 1e-9:
|
|
|
|
|
|
return default
|
|
|
|
|
|
return float(primary) / float(secondary)
|
|
|
|
|
|
|
2026-06-12 13:32:58 +08:00
|
|
|
|
def _get_config_transformer_ratios(self, line_no: int) -> Dict[str, float]:
|
2026-06-26 16:36:02 +08:00
|
|
|
|
"""从设备参数区读取 PT/CT 变比。
|
2026-06-12 13:32:58 +08:00
|
|
|
|
|
2026-06-26 16:36:02 +08:00
|
|
|
|
来源:设备线路定值回读结果中的 transformer_change[]
|
|
|
|
|
|
当前约定:category 为“PT变化/CT变化”(兼容旧的“PT变比/CT变比”),取 value 字段。
|
2026-06-12 13:32:58 +08:00
|
|
|
|
"""
|
|
|
|
|
|
try:
|
2026-06-26 16:36:02 +08:00
|
|
|
|
current = self.device_client.read_line_alarm_setting(line_no)
|
2026-06-12 13:32:58 +08:00
|
|
|
|
except Exception:
|
2026-06-26 16:36:02 +08:00
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
transformer_list = current.get("transformer_change")
|
|
|
|
|
|
if not isinstance(transformer_list, list):
|
|
|
|
|
|
return {}
|
|
|
|
|
|
pt_ratio: Optional[float] = None
|
|
|
|
|
|
ct_ratio: Optional[float] = None
|
|
|
|
|
|
for transformer in transformer_list:
|
|
|
|
|
|
if not isinstance(transformer, dict):
|
2026-06-12 13:32:58 +08:00
|
|
|
|
continue
|
2026-06-26 16:36:02 +08:00
|
|
|
|
category = str(transformer.get("category", "")).strip()
|
|
|
|
|
|
raw_value = transformer.get("value", transformer.get("limit", 1.0))
|
|
|
|
|
|
try:
|
|
|
|
|
|
value = float(raw_value)
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
|
value = 1.0
|
|
|
|
|
|
if value <= 0:
|
|
|
|
|
|
value = 1.0
|
|
|
|
|
|
if category in {"PT变化", "PT变比"}:
|
|
|
|
|
|
pt_ratio = value
|
|
|
|
|
|
if category in {"CT变化", "CT变比"}:
|
|
|
|
|
|
ct_ratio = value
|
|
|
|
|
|
result: Dict[str, float] = {}
|
|
|
|
|
|
if pt_ratio is not None:
|
|
|
|
|
|
result["pt_ratio"] = pt_ratio
|
|
|
|
|
|
if ct_ratio is not None:
|
|
|
|
|
|
result["ct_ratio"] = ct_ratio
|
|
|
|
|
|
return result
|
2026-06-12 13:32:58 +08:00
|
|
|
|
|
2026-05-19 14:47:08 +08:00
|
|
|
|
def _get_line_transformer_ratios(self, line: LineData) -> Dict[str, float]:
|
2026-06-12 13:32:58 +08:00
|
|
|
|
"""确定当前线路使用的 PT/CT 变比。
|
|
|
|
|
|
|
|
|
|
|
|
优先级:
|
|
|
|
|
|
1) 配置文件中指定的变比(若存在);
|
|
|
|
|
|
2) 由设备返回的 pri/sec 数据反算得到的变比(兜底)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
config_ratios = self._get_config_transformer_ratios(line.line_no)
|
|
|
|
|
|
if config_ratios.get("pt_ratio") is not None and config_ratios.get("ct_ratio") is not None:
|
|
|
|
|
|
return {"pt_ratio": float(config_ratios["pt_ratio"]), "ct_ratio": float(config_ratios["ct_ratio"])}
|
|
|
|
|
|
|
2026-05-19 14:47:08 +08:00
|
|
|
|
pri = line.pri_val
|
|
|
|
|
|
sec = line.sec_val
|
|
|
|
|
|
|
|
|
|
|
|
pt_ratio_candidates = [
|
|
|
|
|
|
self._safe_ratio(pri.Ua, sec.Ua, 0.0),
|
|
|
|
|
|
self._safe_ratio(pri.Ub, sec.Ub, 0.0),
|
|
|
|
|
|
self._safe_ratio(pri.Uc, sec.Uc, 0.0),
|
|
|
|
|
|
self._safe_ratio(pri.Uab, sec.Uab, 0.0),
|
|
|
|
|
|
self._safe_ratio(pri.Ubc, sec.Ubc, 0.0),
|
|
|
|
|
|
self._safe_ratio(pri.Uca, sec.Uca, 0.0),
|
|
|
|
|
|
]
|
|
|
|
|
|
ct_ratio_candidates = [
|
|
|
|
|
|
self._safe_ratio(pri.Ia, sec.Ia, 0.0),
|
|
|
|
|
|
self._safe_ratio(pri.Ib, sec.Ib, 0.0),
|
|
|
|
|
|
self._safe_ratio(pri.Ic, sec.Ic, 0.0),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
pt_ratio = next((ratio for ratio in pt_ratio_candidates if ratio > 0), 1.0)
|
|
|
|
|
|
ct_ratio = next((ratio for ratio in ct_ratio_candidates if ratio > 0), 1.0)
|
2026-06-12 13:32:58 +08:00
|
|
|
|
if config_ratios.get("pt_ratio") is not None:
|
|
|
|
|
|
pt_ratio = float(config_ratios["pt_ratio"])
|
|
|
|
|
|
if config_ratios.get("ct_ratio") is not None:
|
|
|
|
|
|
ct_ratio = float(config_ratios["ct_ratio"])
|
2026-05-19 14:47:08 +08:00
|
|
|
|
return {"pt_ratio": pt_ratio, "ct_ratio": ct_ratio}
|
|
|
|
|
|
|
|
|
|
|
|
def _build_primary_from_secondary(self, secondary: ValueGroup, pt_ratio: float, ct_ratio: float) -> ValueGroup:
|
2026-06-12 13:32:58 +08:00
|
|
|
|
"""根据二次值和变比计算一次值。
|
|
|
|
|
|
|
|
|
|
|
|
规则:
|
|
|
|
|
|
- 电压一次值 = 电压二次值 * PT
|
|
|
|
|
|
- 电流一次值 = 电流二次值 * CT
|
|
|
|
|
|
- 功率一次值 = 功率二次值 * PT * CT
|
|
|
|
|
|
- 频率一次值 = 频率二次值
|
|
|
|
|
|
- 功率因数一次值 = 功率因数二次值
|
|
|
|
|
|
"""
|
2026-05-19 14:47:08 +08:00
|
|
|
|
power_ratio = pt_ratio * ct_ratio
|
|
|
|
|
|
return ValueGroup(
|
|
|
|
|
|
Ua=secondary.Ua * pt_ratio,
|
|
|
|
|
|
Ub=secondary.Ub * pt_ratio,
|
|
|
|
|
|
Uc=secondary.Uc * pt_ratio,
|
|
|
|
|
|
Ia=secondary.Ia * ct_ratio,
|
|
|
|
|
|
Ib=secondary.Ib * ct_ratio,
|
|
|
|
|
|
Ic=secondary.Ic * ct_ratio,
|
|
|
|
|
|
Pa=secondary.Pa * power_ratio,
|
|
|
|
|
|
Pb=secondary.Pb * power_ratio,
|
|
|
|
|
|
Pc=secondary.Pc * power_ratio,
|
|
|
|
|
|
Pt=secondary.Pt * power_ratio,
|
|
|
|
|
|
Qa=secondary.Qa * power_ratio,
|
|
|
|
|
|
Qb=secondary.Qb * power_ratio,
|
|
|
|
|
|
Qc=secondary.Qc * power_ratio,
|
|
|
|
|
|
Qt=secondary.Qt * power_ratio,
|
|
|
|
|
|
Sa=secondary.Sa * power_ratio,
|
|
|
|
|
|
Sb=secondary.Sb * power_ratio,
|
|
|
|
|
|
Sc=secondary.Sc * power_ratio,
|
|
|
|
|
|
St=secondary.St * power_ratio,
|
|
|
|
|
|
PFa=secondary.PFa,
|
|
|
|
|
|
PFb=secondary.PFb,
|
|
|
|
|
|
PFc=secondary.PFc,
|
|
|
|
|
|
PFt=secondary.PFt,
|
|
|
|
|
|
Uab=secondary.Uab * pt_ratio,
|
|
|
|
|
|
Ubc=secondary.Ubc * pt_ratio,
|
|
|
|
|
|
Uca=secondary.Uca * pt_ratio,
|
|
|
|
|
|
frq=secondary.frq,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _round_value_group(self, group: ValueGroup) -> ValueGroup:
|
|
|
|
|
|
group_data = group.model_dump()
|
|
|
|
|
|
precision_map = {
|
|
|
|
|
|
"Ua": 3,
|
|
|
|
|
|
"Ub": 3,
|
|
|
|
|
|
"Uc": 3,
|
|
|
|
|
|
"Ia": 3,
|
|
|
|
|
|
"Ib": 3,
|
|
|
|
|
|
"Ic": 3,
|
|
|
|
|
|
"Pa": 2,
|
|
|
|
|
|
"Pb": 2,
|
|
|
|
|
|
"Pc": 2,
|
|
|
|
|
|
"Pt": 2,
|
|
|
|
|
|
"Qa": 2,
|
|
|
|
|
|
"Qb": 2,
|
|
|
|
|
|
"Qc": 2,
|
|
|
|
|
|
"Qt": 2,
|
|
|
|
|
|
"Sa": 2,
|
|
|
|
|
|
"Sb": 2,
|
|
|
|
|
|
"Sc": 2,
|
|
|
|
|
|
"St": 2,
|
|
|
|
|
|
"PFa": 3,
|
|
|
|
|
|
"PFb": 3,
|
|
|
|
|
|
"PFc": 3,
|
|
|
|
|
|
"PFt": 3,
|
|
|
|
|
|
"Uab": 3,
|
|
|
|
|
|
"Ubc": 3,
|
|
|
|
|
|
"Uca": 3,
|
|
|
|
|
|
"frq": 3,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for field_name, precision in precision_map.items():
|
|
|
|
|
|
value = group_data.get(field_name)
|
|
|
|
|
|
if isinstance(value, (int, float)):
|
|
|
|
|
|
group_data[field_name] = round(float(value), precision)
|
|
|
|
|
|
|
|
|
|
|
|
return ValueGroup(**group_data)
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_realtime_precision(self, realtime: RealtimeData) -> RealtimeData:
|
2026-06-12 13:32:58 +08:00
|
|
|
|
"""实时数据归一化:按配置变比计算一次值,并按界面要求控制小数位。"""
|
2026-05-19 14:47:08 +08:00
|
|
|
|
line_list = []
|
|
|
|
|
|
for line in realtime.line_list:
|
|
|
|
|
|
ratios = self._get_line_transformer_ratios(line)
|
|
|
|
|
|
line_list.append(
|
|
|
|
|
|
LineData(
|
|
|
|
|
|
line_no=line.line_no,
|
|
|
|
|
|
pri_val=self._round_value_group(
|
|
|
|
|
|
self._build_primary_from_secondary(
|
|
|
|
|
|
line.sec_val,
|
|
|
|
|
|
ratios["pt_ratio"],
|
|
|
|
|
|
ratios["ct_ratio"],
|
|
|
|
|
|
)
|
|
|
|
|
|
),
|
|
|
|
|
|
sec_val=self._round_value_group(line.sec_val),
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
ai_collect = {
|
|
|
|
|
|
key: round(float(value), 2) if isinstance(value, (int, float)) else value
|
|
|
|
|
|
for key, value in realtime.ai_collect.items()
|
|
|
|
|
|
}
|
|
|
|
|
|
return RealtimeData(line_list=line_list, switch=dict(realtime.switch), ai_collect=ai_collect)
|
|
|
|
|
|
|
2026-05-18 09:12:14 +08:00
|
|
|
|
def get_realtime_data(self) -> RealtimeData:
|
|
|
|
|
|
cached = memory_store.get_realtime()
|
|
|
|
|
|
if cached is not None:
|
2026-05-19 14:47:08 +08:00
|
|
|
|
normalized = self._normalize_realtime_precision(cached)
|
|
|
|
|
|
memory_store.set_realtime(normalized)
|
|
|
|
|
|
return normalized
|
2026-05-18 09:12:14 +08:00
|
|
|
|
realtime = self.device_client.read_realtime_data()
|
2026-05-19 14:47:08 +08:00
|
|
|
|
normalized = self._normalize_realtime_precision(realtime)
|
|
|
|
|
|
memory_store.set_realtime(normalized)
|
|
|
|
|
|
return normalized
|
2026-05-18 09:12:14 +08:00
|
|
|
|
|
|
|
|
|
|
def get_device_status(self) -> DeviceStatus:
|
|
|
|
|
|
cached = memory_store.get_status()
|
|
|
|
|
|
if cached is not None:
|
|
|
|
|
|
return cached
|
|
|
|
|
|
status = self.device_client.read_device_status()
|
|
|
|
|
|
memory_store.set_status(status)
|
|
|
|
|
|
return status
|
|
|
|
|
|
|
2026-06-29 08:45:20 +08:00
|
|
|
|
def startup_self_check(self) -> Dict[str, Any]:
|
|
|
|
|
|
"""开机自检:通过 03H 读取参数区,验证通讯链路。
|
|
|
|
|
|
|
|
|
|
|
|
若通讯失败,生成"开机自检(通讯链路)异常"报警事件,
|
|
|
|
|
|
并将设备状态置为"断开",网络/串口状态清空。
|
|
|
|
|
|
|
|
|
|
|
|
返回值
|
|
|
|
|
|
----
|
|
|
|
|
|
dict
|
|
|
|
|
|
{"success": bool, "status": DeviceStatus}
|
|
|
|
|
|
"""
|
|
|
|
|
|
success = self.device_client.startup_self_check()
|
|
|
|
|
|
if success:
|
|
|
|
|
|
status = self.device_client.read_device_status()
|
|
|
|
|
|
memory_store.set_status(status)
|
|
|
|
|
|
abnormal_items = _collect_startup_status_abnormalities(status)
|
|
|
|
|
|
if abnormal_items:
|
|
|
|
|
|
alarm = AlarmEvent(
|
|
|
|
|
|
event_time=datetime.now(),
|
|
|
|
|
|
line_code=0,
|
|
|
|
|
|
event_type=EVENT_TYPE_SELF_CHECK,
|
|
|
|
|
|
event_code=SELF_CHECK_COMMUNICATION_ERROR_CODE,
|
|
|
|
|
|
event_value=len(abnormal_items),
|
|
|
|
|
|
content=f"开机自检状态异常:{', '.join(abnormal_items)}",
|
|
|
|
|
|
)
|
|
|
|
|
|
alarm.id = self.alarm_repo.save_alarm(alarm)
|
|
|
|
|
|
memory_store.push_alarm(alarm)
|
|
|
|
|
|
return {"success": True, "status": status.model_dump()}
|
|
|
|
|
|
|
|
|
|
|
|
# 通讯失败:生成报警事件
|
|
|
|
|
|
alarm = AlarmEvent(
|
|
|
|
|
|
event_time=datetime.now(),
|
|
|
|
|
|
line_code=0,
|
|
|
|
|
|
event_type=EVENT_TYPE_SELF_CHECK,
|
|
|
|
|
|
event_code=SELF_CHECK_COMMUNICATION_ERROR_CODE,
|
|
|
|
|
|
event_value=0,
|
|
|
|
|
|
content="开机自检(通讯链路)异常",
|
|
|
|
|
|
)
|
|
|
|
|
|
alarm.id = self.alarm_repo.save_alarm(alarm)
|
|
|
|
|
|
memory_store.push_alarm(alarm)
|
|
|
|
|
|
|
|
|
|
|
|
# 设置状态为断开
|
|
|
|
|
|
status = DeviceStatus(
|
|
|
|
|
|
self_check="断开",
|
|
|
|
|
|
net1="断开",
|
|
|
|
|
|
net2="断开",
|
|
|
|
|
|
net3="断开",
|
|
|
|
|
|
net4="断开",
|
|
|
|
|
|
uart1="断开",
|
|
|
|
|
|
uart2="断开",
|
|
|
|
|
|
uart3="断开",
|
|
|
|
|
|
uart4="断开",
|
|
|
|
|
|
frequency_limit="未知",
|
|
|
|
|
|
ct_break="未知",
|
|
|
|
|
|
pt_break="未知",
|
|
|
|
|
|
)
|
|
|
|
|
|
memory_store.set_status(status)
|
|
|
|
|
|
return {"success": False, "status": status.model_dump()}
|
|
|
|
|
|
|
2026-05-19 09:26:51 +08:00
|
|
|
|
def _default_device_config(self) -> Dict[str, Any]:
|
|
|
|
|
|
return {
|
2026-05-18 12:35:27 +08:00
|
|
|
|
"password": "",
|
|
|
|
|
|
"hardware_version": {
|
|
|
|
|
|
"board_version": "B001.001.001",
|
|
|
|
|
|
"display_version": "S001.001.001",
|
|
|
|
|
|
"other_version": "Y001.001.001",
|
|
|
|
|
|
},
|
|
|
|
|
|
"software_version": {
|
|
|
|
|
|
"display_program": "001.001.001",
|
|
|
|
|
|
"communication_program": "001.001.001",
|
|
|
|
|
|
"measurement_program": "001.001.001",
|
|
|
|
|
|
},
|
|
|
|
|
|
"net": [
|
|
|
|
|
|
{"nic": "网卡一", "ip": "192.168.1.10", "mask": "255.255.255.0", "gateway": "192.168.1.1", "protocol": "Modbus TCP"},
|
|
|
|
|
|
{"nic": "网卡二", "ip": "192.168.1.56", "mask": "255.255.255.255", "gateway": "192.168.1.56", "protocol": "Modbus TCP"},
|
|
|
|
|
|
],
|
|
|
|
|
|
"uart": [
|
|
|
|
|
|
{"port": "COM1", "baud": 9600, "parity": "NONE", "data_bits": 8, "stop_bits": 1, "protocol": ""},
|
|
|
|
|
|
{"port": "COM2", "baud": 115200, "parity": "NONE", "data_bits": 8, "stop_bits": 1, "protocol": "Modbus RTU"},
|
|
|
|
|
|
],
|
|
|
|
|
|
}
|
2026-05-19 09:26:51 +08:00
|
|
|
|
|
|
|
|
|
|
def _merge_keyed_items(self, defaults: List[Dict[str, Any]], current: Any, key_field: str) -> List[Dict[str, Any]]:
|
|
|
|
|
|
merged = [dict(item) for item in defaults]
|
|
|
|
|
|
if not isinstance(current, list):
|
|
|
|
|
|
return merged
|
|
|
|
|
|
|
|
|
|
|
|
index_map = {
|
|
|
|
|
|
item.get(key_field): index
|
|
|
|
|
|
for index, item in enumerate(merged)
|
|
|
|
|
|
if isinstance(item, dict) and item.get(key_field) is not None
|
|
|
|
|
|
}
|
|
|
|
|
|
for item in current:
|
|
|
|
|
|
if not isinstance(item, dict):
|
|
|
|
|
|
continue
|
|
|
|
|
|
key = item.get(key_field)
|
|
|
|
|
|
if key in index_map:
|
|
|
|
|
|
merged[index_map[key]].update(item)
|
|
|
|
|
|
else:
|
|
|
|
|
|
merged.append(dict(item))
|
|
|
|
|
|
return merged
|
|
|
|
|
|
|
|
|
|
|
|
def _upsert_keyed_item(self, items: List[Dict[str, Any]], payload: Dict[str, Any], key_field: str) -> List[Dict[str, Any]]:
|
|
|
|
|
|
key = payload.get(key_field)
|
|
|
|
|
|
updated = False
|
|
|
|
|
|
for index, item in enumerate(items):
|
|
|
|
|
|
if item.get(key_field) == key:
|
|
|
|
|
|
items[index] = payload
|
|
|
|
|
|
updated = True
|
|
|
|
|
|
break
|
|
|
|
|
|
if not updated:
|
|
|
|
|
|
items.append(payload)
|
|
|
|
|
|
return items
|
|
|
|
|
|
|
|
|
|
|
|
def _find_keyed_item(self, items: List[Dict[str, Any]], key_field: str, key_value: str) -> Optional[Dict[str, Any]]:
|
|
|
|
|
|
for item in items:
|
|
|
|
|
|
if item.get(key_field) == key_value:
|
|
|
|
|
|
return item
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _load_device_config_with_defaults(self) -> Dict[str, Any]:
|
|
|
|
|
|
current = self.config_repo.read_device_config()
|
|
|
|
|
|
defaults = self._default_device_config()
|
|
|
|
|
|
data = {
|
|
|
|
|
|
"password": current.get("password", "") if isinstance(current, dict) else "",
|
|
|
|
|
|
"hardware_version": dict(defaults["hardware_version"]),
|
|
|
|
|
|
"software_version": dict(defaults["software_version"]),
|
|
|
|
|
|
"net": [],
|
|
|
|
|
|
"uart": [],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if isinstance(current, dict):
|
|
|
|
|
|
if isinstance(current.get("hardware_version"), dict):
|
|
|
|
|
|
data["hardware_version"].update(current["hardware_version"])
|
|
|
|
|
|
if isinstance(current.get("software_version"), dict):
|
|
|
|
|
|
data["software_version"].update(current["software_version"])
|
|
|
|
|
|
data["net"] = self._merge_keyed_items(defaults["net"], current.get("net"), "nic")
|
|
|
|
|
|
data["uart"] = self._merge_keyed_items(defaults["uart"], current.get("uart"), "port")
|
|
|
|
|
|
else:
|
|
|
|
|
|
data["net"] = self._merge_keyed_items(defaults["net"], None, "nic")
|
|
|
|
|
|
data["uart"] = self._merge_keyed_items(defaults["uart"], None, "port")
|
|
|
|
|
|
|
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
def save_device_config(self, payload: DeviceConfigIn) -> Dict[str, Any]:
|
|
|
|
|
|
data = self._load_device_config_with_defaults()
|
|
|
|
|
|
payload_data = payload.model_dump()
|
|
|
|
|
|
data["hardware_version"] = payload_data["hardware_version"]
|
|
|
|
|
|
data["software_version"] = payload_data["software_version"]
|
|
|
|
|
|
data["net"] = self._merge_keyed_items(data["net"], payload_data["net"], "nic")
|
|
|
|
|
|
data["uart"] = self._merge_keyed_items(data["uart"], payload_data["uart"], "port")
|
|
|
|
|
|
if payload.password.strip():
|
|
|
|
|
|
data["password"] = hash_password(payload.password)
|
2026-06-29 08:45:20 +08:00
|
|
|
|
self._save_operation_event(event_code=8, detail="设备密码")
|
|
|
|
|
|
if payload_data.get("net"):
|
|
|
|
|
|
nics = [item.get("nic", "") for item in payload_data["net"] if isinstance(item, dict)]
|
|
|
|
|
|
self._save_operation_event(event_code=7, detail=f"网卡: {', '.join(nics)}")
|
|
|
|
|
|
if payload_data.get("uart"):
|
|
|
|
|
|
ports = [item.get("port", "") for item in payload_data["uart"] if isinstance(item, dict)]
|
|
|
|
|
|
self._save_operation_event(event_code=7, detail=f"串口: {', '.join(ports)}")
|
2026-05-19 09:26:51 +08:00
|
|
|
|
path = self.config_repo.write_device_config(data)
|
|
|
|
|
|
device_result = self.device_client.send_device_config(payload)
|
|
|
|
|
|
return {"save_path": f"/config/{path.name}", **device_result}
|
|
|
|
|
|
|
2026-05-19 14:47:08 +08:00
|
|
|
|
def save_device_password(self, payload: DevicePasswordUpdateIn) -> Dict[str, Any]:
|
|
|
|
|
|
data = self._load_device_config_with_defaults()
|
|
|
|
|
|
data["password"] = hash_password(payload.password)
|
|
|
|
|
|
path = self.config_repo.write_device_config(data)
|
2026-06-29 08:45:20 +08:00
|
|
|
|
self._save_operation_event(event_code=8, detail="设备密码")
|
2026-05-19 14:47:08 +08:00
|
|
|
|
return {"save_path": f"/config/{path.name}", "target": "password", "send_status": "成功"}
|
|
|
|
|
|
|
2026-05-19 09:26:51 +08:00
|
|
|
|
def get_device_config(self) -> Dict[str, Any]:
|
|
|
|
|
|
data = self._load_device_config_with_defaults()
|
2026-05-18 12:35:27 +08:00
|
|
|
|
# 不返回已保存的哈希密码,避免前端把哈希串直接显示到设置界面
|
|
|
|
|
|
data["password"] = ""
|
|
|
|
|
|
return data
|
|
|
|
|
|
|
2026-05-19 09:26:51 +08:00
|
|
|
|
def get_net_config(self, nic: str) -> Dict[str, Any]:
|
2026-06-26 17:48:42 +08:00
|
|
|
|
return self.device_client.read_net_config(nic)
|
2026-05-19 09:26:51 +08:00
|
|
|
|
|
2026-06-26 19:02:33 +08:00
|
|
|
|
def save_net_config(self, payload: NetConfigItem | List[NetConfigItem]) -> Dict[str, Any]:
|
|
|
|
|
|
if isinstance(payload, list):
|
2026-06-29 08:45:20 +08:00
|
|
|
|
nics = [item.nic for item in payload]
|
2026-06-26 19:02:33 +08:00
|
|
|
|
results = [self.device_client.send_net_config(item) for item in payload]
|
2026-06-29 08:45:20 +08:00
|
|
|
|
self._save_operation_event(event_code=7, detail=f"网卡: {', '.join(nics)}")
|
2026-06-26 19:02:33 +08:00
|
|
|
|
return {"send_status": "成功", "target": "net_batch", "items": len(results), "results": results}
|
2026-06-29 08:45:20 +08:00
|
|
|
|
result = self.device_client.send_net_config(payload)
|
|
|
|
|
|
self._save_operation_event(event_code=7, detail=f"网卡: {payload.nic}")
|
|
|
|
|
|
return result
|
2026-05-19 09:26:51 +08:00
|
|
|
|
|
|
|
|
|
|
def get_uart_config(self, port: str) -> Dict[str, Any]:
|
|
|
|
|
|
device_config = self._load_device_config_with_defaults()
|
|
|
|
|
|
item = self._find_keyed_item(device_config["uart"], "port", port)
|
|
|
|
|
|
if item is not None:
|
|
|
|
|
|
return item
|
|
|
|
|
|
return {"port": port, "baud": 9600, "parity": "NONE", "data_bits": 8, "stop_bits": 1, "protocol": ""}
|
|
|
|
|
|
|
2026-06-26 19:02:33 +08:00
|
|
|
|
def save_uart_config(self, payload: UartConfigItem | List[UartConfigItem]) -> Dict[str, Any]:
|
2026-05-19 09:26:51 +08:00
|
|
|
|
device_config = self._load_device_config_with_defaults()
|
2026-06-26 19:02:33 +08:00
|
|
|
|
if isinstance(payload, list):
|
|
|
|
|
|
results = []
|
2026-06-29 08:45:20 +08:00
|
|
|
|
ports = []
|
2026-06-26 19:02:33 +08:00
|
|
|
|
for item in payload:
|
|
|
|
|
|
device_config["uart"] = self._upsert_keyed_item(device_config["uart"], item.model_dump(), "port")
|
2026-06-29 08:45:20 +08:00
|
|
|
|
ports.append(item.port)
|
2026-06-26 19:02:33 +08:00
|
|
|
|
results.append({"target": "uart", "port": item.port, "send_status": "成功"})
|
|
|
|
|
|
path = self.config_repo.write_device_config(device_config)
|
2026-06-29 08:45:20 +08:00
|
|
|
|
self._save_operation_event(event_code=7, detail=f"串口: {', '.join(ports)}")
|
2026-06-26 19:02:33 +08:00
|
|
|
|
return {
|
|
|
|
|
|
"save_path": f"/config/{path.name}",
|
|
|
|
|
|
"target": "uart_batch",
|
|
|
|
|
|
"items": len(results),
|
|
|
|
|
|
"send_status": "成功",
|
|
|
|
|
|
"results": results,
|
|
|
|
|
|
}
|
2026-05-19 09:26:51 +08:00
|
|
|
|
device_config["uart"] = self._upsert_keyed_item(device_config["uart"], payload.model_dump(), "port")
|
|
|
|
|
|
path = self.config_repo.write_device_config(device_config)
|
2026-06-29 08:45:20 +08:00
|
|
|
|
self._save_operation_event(event_code=7, detail=f"串口: {payload.port}")
|
2026-05-19 09:26:51 +08:00
|
|
|
|
return {"save_path": f"/config/{path.name}", "target": "uart", "port": payload.port, "send_status": "成功"}
|
|
|
|
|
|
|
2026-05-18 09:12:14 +08:00
|
|
|
|
def save_channel_config(self, payload: ChannelConfigIn) -> Dict[str, Any]:
|
|
|
|
|
|
device_result = self.device_client.send_channel_config(payload)
|
2026-06-29 08:45:20 +08:00
|
|
|
|
if payload.ai_channel:
|
|
|
|
|
|
ai_chs = [str(item.ch) for item in payload.ai_channel]
|
|
|
|
|
|
self._save_operation_event(event_code=5, detail=f"通道: {', '.join(ai_chs)}")
|
|
|
|
|
|
if payload.ao_channel:
|
|
|
|
|
|
ao_chs = [str(item.ch) for item in payload.ao_channel]
|
|
|
|
|
|
self._save_operation_event(event_code=6, detail=f"通道: {', '.join(ao_chs)}")
|
2026-06-26 16:36:02 +08:00
|
|
|
|
return device_result
|
2026-05-18 09:12:14 +08:00
|
|
|
|
|
2026-05-18 12:35:27 +08:00
|
|
|
|
def get_channel_config(self) -> Dict[str, Any]:
|
2026-06-26 16:36:02 +08:00
|
|
|
|
return self.device_client.read_channel_config()
|
2026-05-18 12:35:27 +08:00
|
|
|
|
|
2026-05-18 17:05:23 +08:00
|
|
|
|
def _default_line_alarm_setting(self, line_no: int) -> Dict[str, Any]:
|
2026-05-18 12:35:27 +08:00
|
|
|
|
return {
|
2026-05-18 17:05:23 +08:00
|
|
|
|
"line_no": line_no,
|
2026-05-18 12:35:27 +08:00
|
|
|
|
"over_limit_alarm": [
|
|
|
|
|
|
{"category": "电压", "limit": 180, "delay": 180, "output_node": "开出1", "enabled": True},
|
|
|
|
|
|
{"category": "电流", "limit": 180, "delay": 180, "output_node": "开出1", "enabled": True},
|
|
|
|
|
|
{"category": "差流", "limit": 180, "delay": 180, "output_node": "开出1", "enabled": False},
|
2026-06-26 16:36:02 +08:00
|
|
|
|
{"category": "功率", "limit": 180, "delay": 180, "output_node": "开出1", "enabled": False},
|
2026-05-18 12:35:27 +08:00
|
|
|
|
{"category": "频率", "limit": 180, "delay": 180, "output_node": "开出1", "enabled": False},
|
|
|
|
|
|
],
|
2026-06-26 16:36:02 +08:00
|
|
|
|
"fault_alarm": [
|
|
|
|
|
|
{"category": "PT断线", "limit": 0, "delay": 180, "output_node": "开出1", "enabled": True},
|
|
|
|
|
|
{"category": "CT断线", "limit": 0, "delay": 180, "output_node": "开出2", "enabled": False},
|
|
|
|
|
|
],
|
2026-06-12 11:32:22 +08:00
|
|
|
|
"transformer_change": [
|
2026-06-12 13:32:58 +08:00
|
|
|
|
{"category": "PT变化", "value": 1.0, "enabled": False},
|
|
|
|
|
|
{"category": "CT变化", "value": 1.0, "enabled": False},
|
2026-06-12 11:32:22 +08:00
|
|
|
|
],
|
2026-05-18 12:35:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 17:05:23 +08:00
|
|
|
|
def _normalize_line_alarm_settings(self, raw: Any) -> List[Dict[str, Any]]:
|
|
|
|
|
|
if isinstance(raw, list):
|
|
|
|
|
|
return [item for item in raw if isinstance(item, dict)]
|
|
|
|
|
|
if isinstance(raw, dict):
|
|
|
|
|
|
return [raw]
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
def save_line_alarm_setting(self, payload: LineAlarmSettingIn) -> Dict[str, Any]:
|
|
|
|
|
|
device_result = self.device_client.send_line_alarm_setting(payload)
|
2026-06-29 08:45:20 +08:00
|
|
|
|
self._save_operation_event(event_code=3, line_code=payload.line_no)
|
2026-06-26 16:36:02 +08:00
|
|
|
|
return device_result
|
2026-05-18 17:05:23 +08:00
|
|
|
|
|
|
|
|
|
|
def get_line_alarm_setting(self, line_no: int = 1) -> Dict[str, Any]:
|
2026-06-26 16:36:02 +08:00
|
|
|
|
merged = self._default_line_alarm_setting(line_no)
|
2026-06-29 08:45:20 +08:00
|
|
|
|
try:
|
|
|
|
|
|
current = self.device_client.read_line_alarm_setting(line_no)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
self._save_operation_event(event_code=2, line_code=line_no, detail=f"线路{line_no}:{exc}")
|
|
|
|
|
|
current = None
|
2026-06-26 16:36:02 +08:00
|
|
|
|
if isinstance(current, dict):
|
|
|
|
|
|
merged.update(current)
|
|
|
|
|
|
normalized_transformer: List[Dict[str, Any]] = []
|
|
|
|
|
|
for transformer in merged.get("transformer_change", []):
|
|
|
|
|
|
if not isinstance(transformer, dict):
|
|
|
|
|
|
continue
|
|
|
|
|
|
category = transformer.get("category", "")
|
|
|
|
|
|
enabled = bool(transformer.get("enabled", False))
|
|
|
|
|
|
raw_value = transformer.get("value", transformer.get("limit", 1.0))
|
|
|
|
|
|
try:
|
|
|
|
|
|
value = float(raw_value)
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
|
value = 1.0
|
|
|
|
|
|
normalized_transformer.append({"category": category, "value": value, "enabled": enabled})
|
|
|
|
|
|
merged["transformer_change"] = normalized_transformer
|
|
|
|
|
|
return merged
|
2026-05-18 17:05:23 +08:00
|
|
|
|
|
2026-05-18 09:12:14 +08:00
|
|
|
|
def save_ai_alarm_setting(self, payload: List[AiAlarmSettingIn]) -> Dict[str, Any]:
|
2026-06-26 16:36:02 +08:00
|
|
|
|
payload_data = [item.model_dump() for item in payload]
|
2026-06-29 08:45:20 +08:00
|
|
|
|
result = self.device_client.send_ai_alarm_setting(payload_data)
|
|
|
|
|
|
chs = [str(item.channel_no) for item in payload]
|
|
|
|
|
|
self._save_operation_event(event_code=4, detail=f"通道: {', '.join(chs)}")
|
|
|
|
|
|
return result
|
2026-05-18 09:12:14 +08:00
|
|
|
|
|
2026-05-18 12:35:27 +08:00
|
|
|
|
def get_ai_alarm_setting(self) -> List[Dict[str, Any]]:
|
2026-06-26 16:36:02 +08:00
|
|
|
|
return self.device_client.read_ai_alarm_setting()
|
2026-05-18 12:35:27 +08:00
|
|
|
|
|
2026-05-18 09:12:14 +08:00
|
|
|
|
def save_system_config(self, payload: SystemConfigIn) -> Dict[str, Any]:
|
|
|
|
|
|
return self.device_client.send_system_config(payload)
|
|
|
|
|
|
|
2026-05-18 12:35:27 +08:00
|
|
|
|
def get_system_config(self) -> Dict[str, Any]:
|
2026-06-26 16:36:02 +08:00
|
|
|
|
return self.device_client.read_system_config()
|
|
|
|
|
|
|
|
|
|
|
|
def save_time_sync_config(self, payload: TimeSyncConfigIn) -> Dict[str, Any]:
|
|
|
|
|
|
return self.device_client.send_time_sync_config(payload)
|
|
|
|
|
|
|
|
|
|
|
|
def get_time_sync_config(self) -> Dict[str, Any]:
|
|
|
|
|
|
return self.device_client.read_time_sync_config()
|
2026-05-18 12:35:27 +08:00
|
|
|
|
|
|
|
|
|
|
def list_alarms(
|
|
|
|
|
|
self,
|
|
|
|
|
|
page: int,
|
|
|
|
|
|
size: int,
|
2026-06-26 17:48:42 +08:00
|
|
|
|
line_code: Optional[int] = None,
|
|
|
|
|
|
event_type: Optional[int] = None,
|
2026-05-18 12:35:27 +08:00
|
|
|
|
start_time: Optional[datetime] = None,
|
|
|
|
|
|
end_time: Optional[datetime] = None,
|
|
|
|
|
|
) -> List[Dict[str, Any]]:
|
|
|
|
|
|
return self.alarm_repo.list_alarms(
|
|
|
|
|
|
page=page,
|
|
|
|
|
|
size=size,
|
2026-06-26 17:48:42 +08:00
|
|
|
|
line_code=line_code,
|
|
|
|
|
|
event_type=event_type,
|
2026-05-18 12:35:27 +08:00
|
|
|
|
start_time=start_time,
|
|
|
|
|
|
end_time=end_time,
|
|
|
|
|
|
)
|
2026-05-18 09:12:14 +08:00
|
|
|
|
|
2026-06-29 08:45:20 +08:00
|
|
|
|
def _save_operation_event(
|
|
|
|
|
|
self,
|
|
|
|
|
|
event_code: int,
|
|
|
|
|
|
line_code: int = 0,
|
|
|
|
|
|
event_value: int = 0,
|
|
|
|
|
|
detail: str = "",
|
|
|
|
|
|
) -> AlarmEvent:
|
|
|
|
|
|
"""通用操作事件生成保存方法(同步)。
|
|
|
|
|
|
|
|
|
|
|
|
将主站侧操作类事件保存到 SQLite 并推入内存缓存。
|
|
|
|
|
|
返回保存后的 AlarmEvent 对象(含数据库自增 id),调用方可进一步广播。
|
|
|
|
|
|
|
|
|
|
|
|
参数
|
|
|
|
|
|
----
|
|
|
|
|
|
event_code : int
|
|
|
|
|
|
操作事件代码,取值 1~8,对应含义见 OPERATION_EVENT_CODE
|
|
|
|
|
|
line_code : int
|
|
|
|
|
|
关联线路编号,0 表示装置级
|
|
|
|
|
|
event_value : int
|
|
|
|
|
|
事件附带数值,如分合闸通道号
|
|
|
|
|
|
detail : str
|
|
|
|
|
|
附加描述(如网卡名、端口号、参数名等)
|
|
|
|
|
|
|
|
|
|
|
|
返回值
|
|
|
|
|
|
----
|
|
|
|
|
|
AlarmEvent
|
|
|
|
|
|
已保存的事件对象(id 已回填)
|
|
|
|
|
|
"""
|
|
|
|
|
|
content = _generate_operation_event_content(event_code, line_code, detail)
|
|
|
|
|
|
alarm = AlarmEvent(
|
|
|
|
|
|
event_time=datetime.now(),
|
|
|
|
|
|
line_code=line_code,
|
|
|
|
|
|
event_type=EVENT_TYPE_OPERATION,
|
|
|
|
|
|
event_code=event_code,
|
|
|
|
|
|
event_value=event_value,
|
|
|
|
|
|
content=content,
|
|
|
|
|
|
)
|
|
|
|
|
|
alarm.id = self.alarm_repo.save_alarm(alarm)
|
|
|
|
|
|
memory_store.push_alarm(alarm)
|
|
|
|
|
|
return alarm
|
|
|
|
|
|
|
2026-05-18 09:12:14 +08:00
|
|
|
|
def switch_control(self, payload: SwitchControlIn) -> Dict[str, Any]:
|
2026-06-29 08:45:20 +08:00
|
|
|
|
result = self.device_client.send_switch_control(payload)
|
|
|
|
|
|
action_label = "合闸" if payload.action == 1 else "分闸"
|
|
|
|
|
|
self._save_operation_event(
|
|
|
|
|
|
event_code=1,
|
|
|
|
|
|
event_value=payload.ch,
|
|
|
|
|
|
detail=f"CH{payload.ch} {action_label}",
|
|
|
|
|
|
)
|
|
|
|
|
|
return result
|
2026-05-18 09:12:14 +08:00
|
|
|
|
|
2026-06-26 16:36:02 +08:00
|
|
|
|
async def poll_time_sync_once(self) -> None:
|
|
|
|
|
|
time_sync = self.device_client.read_time_sync_config()
|
|
|
|
|
|
await ws_manager.broadcast("time-sync", {"type": "time_sync", "data": time_sync})
|
|
|
|
|
|
|
2026-05-18 09:12:14 +08:00
|
|
|
|
async def poll_device_once(self) -> None:
|
2026-05-19 14:47:08 +08:00
|
|
|
|
realtime = self._normalize_realtime_precision(self.device_client.read_realtime_data())
|
2026-05-18 09:12:14 +08:00
|
|
|
|
status = self.device_client.read_device_status()
|
|
|
|
|
|
alarms = self.device_client.read_alarm_events()
|
|
|
|
|
|
|
|
|
|
|
|
memory_store.set_realtime(realtime)
|
|
|
|
|
|
memory_store.set_status(status)
|
|
|
|
|
|
await ws_manager.broadcast("real-time", {"type": "real_time", "data": realtime.model_dump()})
|
2026-06-29 08:45:20 +08:00
|
|
|
|
await ws_manager.broadcast("status", {"type": "status", "data": status.model_dump()})
|
2026-05-18 09:12:14 +08:00
|
|
|
|
|
|
|
|
|
|
for alarm in alarms:
|
|
|
|
|
|
alarm.id = self.alarm_repo.save_alarm(alarm)
|
|
|
|
|
|
memory_store.push_alarm(alarm)
|
|
|
|
|
|
await ws_manager.broadcast("alarm", alarm.model_dump(mode="json"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
platform_service = PlatformService()
|