393 lines
14 KiB
Python
393 lines
14 KiB
Python
import json
|
||
from dataclasses import dataclass, field
|
||
from pathlib import Path
|
||
from typing import Any, Dict, List, Optional
|
||
import re
|
||
|
||
|
||
@dataclass
|
||
class InfluxQueryConfig:
|
||
bucket: str = ""
|
||
measurement: str = ""
|
||
fields: List[str] = field(default_factory=list)
|
||
filters: Dict[str, str] = field(default_factory=dict)
|
||
timeRange: str = "-1h"
|
||
aggregate: str = ""
|
||
windowPeriod: str = "" # e.g. 1m
|
||
|
||
|
||
@dataclass
|
||
class TableOptions:
|
||
firstColumn: str = "time" # time | seconds
|
||
firstTitle: str = "" # custom header for the first column
|
||
titles: Dict[str, str] = field(default_factory=dict) # field -> title
|
||
|
||
|
||
@dataclass
|
||
class DbConnectionConfig:
|
||
engine: str = "mysql" # mysql | sqlserver | sqlite
|
||
host: str = "localhost"
|
||
port: int = 3306
|
||
database: str = ""
|
||
username: str = ""
|
||
password: str = ""
|
||
|
||
|
||
@dataclass
|
||
class PlaceholderConfig:
|
||
key: str
|
||
type: str # text | table | chart | cell | scriptTable | dbText
|
||
label: str = ""
|
||
title: str = ""
|
||
value: str = "" # for text
|
||
dbQuery: str = "" # for dbText: SQL query string
|
||
influx: Optional[InfluxQueryConfig] = None
|
||
chart: Dict[str, Any] = field(default_factory=dict)
|
||
table: TableOptions = field(default_factory=TableOptions)
|
||
grid: List[List[str]] = field(default_factory=list) # for manual cells
|
||
|
||
|
||
@dataclass
|
||
class InfluxGlobalConfig:
|
||
url: str = ""
|
||
org: str = ""
|
||
token: str = ""
|
||
username: str = "" # for UI login fallback
|
||
password: str = "" # for UI login fallback
|
||
landingUrl: str = "" # optional: post-login destination (full URL or path)
|
||
bucket: str = "" # InfluxDB bucket name for monitoring
|
||
measurement: str = "" # InfluxDB measurement name for monitoring
|
||
|
||
|
||
@dataclass
|
||
class ExperimentProcess:
|
||
"""实验流程表格数据"""
|
||
headers: List[str] = field(default_factory=list) # 表头
|
||
rows: List[List[str]] = field(default_factory=list) # 数据行
|
||
scriptFile: str = "" # Python脚本文件(base64编码)
|
||
scriptName: str = "" # 脚本文件名(用于显示)
|
||
remark: str = "" # 实验备注
|
||
|
||
|
||
# ===== 新增:TCP Modbus 与设备阈值/报警配置 =====
|
||
@dataclass
|
||
class TcpModbusConfig:
|
||
ip: str = "127.0.0.1"
|
||
port: int = 502
|
||
|
||
|
||
@dataclass
|
||
class ConfigServiceSettings:
|
||
"""配置服务连接设置"""
|
||
host: str = "127.0.0.1"
|
||
port: int = 5000
|
||
configPath: str = "config.json"
|
||
|
||
|
||
@dataclass
|
||
class ThresholdConfig:
|
||
name: str = "" # 阈值名称,如 压力上限
|
||
register: int = 0 # 读取/比较的寄存器地址
|
||
regType: str = "holding" # 地址类型: holding | input | coil
|
||
value: float = 0.0 # 阈值
|
||
|
||
|
||
@dataclass
|
||
class DeviceConfig:
|
||
deviceName: str = "" # 设备名
|
||
thresholds: List[ThresholdConfig] = field(default_factory=list)
|
||
alarmRegister: int = 0 # 设备状态/报警等寄存器地址
|
||
alarmType: str = "holding" # 地址类型: holding | input | coil
|
||
|
||
|
||
@dataclass
|
||
class GlobalParametersConfig:
|
||
"""全局参数配置,用于存储可在报告模板中引用的变量"""
|
||
parameters: Dict[str, str] = field(default_factory=dict)
|
||
|
||
|
||
@dataclass
|
||
class InfluxGlobalConfigWrapper:
|
||
pass
|
||
|
||
|
||
@dataclass
|
||
class AppConfig:
|
||
influx: InfluxGlobalConfig = field(default_factory=InfluxGlobalConfig)
|
||
placeholders: Dict[str, PlaceholderConfig] = field(default_factory=dict)
|
||
experimentProcess: ExperimentProcess = field(default_factory=ExperimentProcess) # 实验流程
|
||
tcpModbus: TcpModbusConfig = field(default_factory=TcpModbusConfig)
|
||
devices: List[DeviceConfig] = field(default_factory=list)
|
||
configService: ConfigServiceSettings = field(default_factory=ConfigServiceSettings) # 配置服务设置
|
||
db: DbConnectionConfig = field(default_factory=DbConnectionConfig)
|
||
globalParameters: GlobalParametersConfig = field(default_factory=GlobalParametersConfig) # 全局参数
|
||
|
||
@staticmethod
|
||
def from_dict(data: Dict[str, Any]) -> "AppConfig":
|
||
influx_data = data.get("influx", {})
|
||
influx = InfluxGlobalConfig(
|
||
url=influx_data.get("url", ""),
|
||
org=influx_data.get("org", ""),
|
||
token=influx_data.get("token", ""),
|
||
username=influx_data.get("username", ""),
|
||
password=influx_data.get("password", ""),
|
||
landingUrl=influx_data.get("landingUrl", ""),
|
||
bucket=influx_data.get("bucket", ""),
|
||
measurement=influx_data.get("measurement", ""),
|
||
)
|
||
placeholders: Dict[str, PlaceholderConfig] = {}
|
||
for k, v in data.get("placeholders", {}).items():
|
||
influx_cfg = None
|
||
if "influx" in v and isinstance(v["influx"], dict):
|
||
inf = v["influx"]
|
||
influx_cfg = InfluxQueryConfig(
|
||
bucket=inf.get("bucket", ""),
|
||
measurement=inf.get("measurement", ""),
|
||
fields=inf.get("fields", []),
|
||
filters=inf.get("filters", {}),
|
||
timeRange=inf.get("timeRange", "-1h"),
|
||
aggregate=inf.get("aggregate", ""),
|
||
windowPeriod=inf.get("windowPeriod", ""),
|
||
)
|
||
table_opts = TableOptions()
|
||
if "table" in v and isinstance(v["table"], dict):
|
||
to = v["table"]
|
||
table_opts.firstColumn = to.get("firstColumn", table_opts.firstColumn)
|
||
table_opts.firstTitle = to.get("firstTitle", table_opts.firstTitle)
|
||
table_opts.titles = dict(to.get("titles", {}))
|
||
placeholders[k] = PlaceholderConfig(
|
||
key=k,
|
||
type=v.get("type", "text"),
|
||
label=v.get("label", k),
|
||
title=v.get("title", ""),
|
||
value=v.get("value", ""),
|
||
dbQuery=v.get("dbQuery", ""),
|
||
influx=influx_cfg,
|
||
chart=v.get("chart", {}),
|
||
table=table_opts,
|
||
grid=v.get("grid", []),
|
||
)
|
||
# 新增:TCP Modbus
|
||
tcp = TcpModbusConfig(**data.get("tcpModbus", {}))
|
||
# 新增:设备列表
|
||
devices: List[DeviceConfig] = []
|
||
for d in data.get("devices", []):
|
||
ths: List[ThresholdConfig] = []
|
||
for t in d.get("thresholds", []):
|
||
ths.append(
|
||
ThresholdConfig(
|
||
name=t.get("name", ""),
|
||
register=int(t.get("register", 0)),
|
||
regType=t.get("regType", "holding"),
|
||
value=float(t.get("value", 0.0)),
|
||
)
|
||
)
|
||
devices.append(
|
||
DeviceConfig(
|
||
deviceName=d.get("deviceName", ""),
|
||
thresholds=ths,
|
||
alarmRegister=int(d.get("alarmRegister", 0)),
|
||
alarmType=d.get("alarmType", "holding"),
|
||
)
|
||
)
|
||
|
||
# 实验流程
|
||
exp_data = data.get("experimentProcess", {})
|
||
experiment_process = ExperimentProcess(
|
||
headers=exp_data.get("headers", []),
|
||
rows=exp_data.get("rows", []),
|
||
scriptFile=exp_data.get("scriptFile", ""),
|
||
scriptName=exp_data.get("scriptName", ""),
|
||
remark=exp_data.get("remark", "")
|
||
)
|
||
|
||
# 配置服务设置
|
||
config_service_data = data.get("configService", {})
|
||
config_service = ConfigServiceSettings(
|
||
host=config_service_data.get("host", "127.0.0.1"),
|
||
port=int(config_service_data.get("port", 5000)),
|
||
configPath=config_service_data.get("configPath", "config.json")
|
||
)
|
||
|
||
db_data = data.get("db", {})
|
||
db_conn = DbConnectionConfig(
|
||
engine=str(db_data.get("engine", "mysql") or "mysql"),
|
||
host=str(db_data.get("host", "localhost") or "localhost"),
|
||
port=int(db_data.get("port", 3306) or 3306),
|
||
database=str(db_data.get("database", "") or ""),
|
||
username=str(db_data.get("username", "") or ""),
|
||
password=str(db_data.get("password", "") or ""),
|
||
)
|
||
|
||
# 全局参数
|
||
global_params_data = data.get("globalParameters", {})
|
||
global_params = GlobalParametersConfig(
|
||
parameters=global_params_data.get("parameters", {})
|
||
)
|
||
|
||
return AppConfig(
|
||
influx=influx,
|
||
placeholders=placeholders,
|
||
experimentProcess=experiment_process,
|
||
tcpModbus=tcp,
|
||
devices=devices,
|
||
configService=config_service,
|
||
db=db_conn,
|
||
globalParameters=global_params,
|
||
)
|
||
|
||
def _natural_sorted_keys(self) -> List[str]:
|
||
def key_fn(k: str) -> tuple[str, int, str]:
|
||
m = re.match(r"([a-zA-Z]+)(\d+)$", k)
|
||
if m:
|
||
return (m.group(1), int(m.group(2)), k)
|
||
return (k, 10**9, k)
|
||
return sorted(self.placeholders.keys(), key=key_fn)
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
result: Dict[str, Any] = {
|
||
"influx": {
|
||
"url": self.influx.url,
|
||
"org": self.influx.org,
|
||
"token": self.influx.token,
|
||
"username": self.influx.username,
|
||
"password": self.influx.password,
|
||
"landingUrl": self.influx.landingUrl,
|
||
"bucket": self.influx.bucket,
|
||
"measurement": self.influx.measurement,
|
||
},
|
||
"placeholders": {},
|
||
"tcpModbus": {
|
||
"ip": self.tcpModbus.ip,
|
||
"port": self.tcpModbus.port,
|
||
},
|
||
"devices": [
|
||
{
|
||
"deviceName": d.deviceName,
|
||
"alarmRegister": d.alarmRegister,
|
||
"alarmType": d.alarmType,
|
||
"thresholds": [
|
||
{
|
||
"name": t.name,
|
||
"register": t.register,
|
||
"regType": t.regType,
|
||
"value": t.value,
|
||
} for t in d.thresholds
|
||
]
|
||
} for d in self.devices
|
||
],
|
||
"db": {
|
||
"engine": self.db.engine,
|
||
"host": self.db.host,
|
||
"port": self.db.port,
|
||
"database": self.db.database,
|
||
"username": self.db.username,
|
||
"password": self.db.password,
|
||
},
|
||
}
|
||
for k in self._natural_sorted_keys():
|
||
ph = self.placeholders[k]
|
||
ph_dict: Dict[str, Any] = {
|
||
"type": ph.type,
|
||
"label": ph.label or k,
|
||
"title": ph.title,
|
||
"value": ph.value,
|
||
"dbQuery": ph.dbQuery,
|
||
"chart": ph.chart or {},
|
||
}
|
||
if ph.influx:
|
||
ph_dict["influx"] = {
|
||
"bucket": ph.influx.bucket,
|
||
"measurement": ph.influx.measurement,
|
||
"fields": ph.influx.fields,
|
||
"filters": ph.influx.filters,
|
||
"timeRange": ph.influx.timeRange,
|
||
"aggregate": ph.influx.aggregate,
|
||
"windowPeriod": ph.influx.windowPeriod,
|
||
}
|
||
if ph.type == "table":
|
||
ph_dict["table"] = {
|
||
"firstColumn": ph.table.firstColumn,
|
||
"firstTitle": ph.table.firstTitle,
|
||
"titles": ph.table.titles,
|
||
}
|
||
if ph.type == "cell":
|
||
ph_dict["grid"] = ph.grid
|
||
result["placeholders"][k] = ph_dict
|
||
|
||
# 实验流程
|
||
result["experimentProcess"] = {
|
||
"headers": self.experimentProcess.headers,
|
||
"rows": self.experimentProcess.rows,
|
||
"scriptFile": self.experimentProcess.scriptFile,
|
||
"scriptName": self.experimentProcess.scriptName,
|
||
"remark": self.experimentProcess.remark
|
||
}
|
||
|
||
# 配置服务设置
|
||
result["configService"] = {
|
||
"host": self.configService.host,
|
||
"port": self.configService.port,
|
||
"configPath": self.configService.configPath
|
||
}
|
||
|
||
# 全局参数
|
||
result["globalParameters"] = {
|
||
"parameters": self.globalParameters.parameters
|
||
}
|
||
|
||
return result
|
||
|
||
@staticmethod
|
||
def load(path: Path) -> "AppConfig":
|
||
with path.open("r", encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
return AppConfig.from_dict(data)
|
||
|
||
def save(self, path: Path) -> None:
|
||
# 如果文件已存在,先创建备份
|
||
if path.exists():
|
||
import shutil
|
||
backup_path = path.with_suffix('.json.bak')
|
||
try:
|
||
shutil.copy2(path, backup_path)
|
||
except Exception:
|
||
pass # 备份失败不影响保存
|
||
|
||
# 保存配置
|
||
with path.open("w", encoding="utf-8") as f:
|
||
json.dump(self.to_dict(), f, ensure_ascii=False, indent=2)
|
||
|
||
def ensure_placeholder(self, key: str, type_hint: str) -> PlaceholderConfig:
|
||
if key not in self.placeholders:
|
||
default = PlaceholderConfig(
|
||
key=key,
|
||
type=type_hint,
|
||
label=key,
|
||
title=key if type_hint in ("table", "chart") else "",
|
||
influx=InfluxQueryConfig() if type_hint in ("table", "chart") else None,
|
||
grid=[[""]] if type_hint == "cell" else [],
|
||
dbQuery="" if type_hint == "dbText" else "",
|
||
)
|
||
self.placeholders[key] = default
|
||
ph = self.placeholders[key]
|
||
if ph.type != type_hint:
|
||
ph.type = type_hint
|
||
if type_hint in ("table", "chart"):
|
||
ph.influx = ph.influx or InfluxQueryConfig()
|
||
if type_hint == "table" and ph.table is None:
|
||
ph.table = TableOptions()
|
||
else:
|
||
ph.influx = None
|
||
if type_hint == "cell":
|
||
ph.grid = ph.grid or [[""]]
|
||
else:
|
||
ph.grid = []
|
||
if type_hint == "dbText":
|
||
ph.dbQuery = ph.dbQuery or ""
|
||
if type_hint not in ("table", "chart"):
|
||
ph.table = TableOptions()
|
||
if type_hint not in ("table", "chart"):
|
||
ph.title = ph.title if type_hint in ("table", "chart") else ""
|
||
return ph
|