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 # 备份失败不影响保存 # 保存配置 # 使用 newline='\n' 确保跨平台换行符一致(LF),避免 Windows CRLF 导致的问题 with path.open("w", encoding="utf-8", newline='\n') as f: json.dump(self.to_dict(), f, ensure_ascii=False, indent=2, sort_keys=False) 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