# -*- coding: utf-8 -*- """点位字典 CSV 加载器:CSV → 内存模型(Point 记录列表)。 复用化工 AI 边缘网关的点位字典机制,改为模板化读取: - 表头必须与 schema.CSV_HEADERS 一致(列顺序不重要,按列名匹配)。 - 行为宽松:缺失列/多余列由校验器(validator.py)统一报告,加载器不做丢弃。 """ from __future__ import annotations import csv from dataclasses import dataclass, field from typing import Any, Dict, List, Optional from . import schema @dataclass class Point: """单条测点记录(对应点位字典 CSV 一行)。""" device_id: str point_id: str name: str unit: str data_type: str sample_rate: int quality_code: bool = True opc_node: Optional[str] = None protocol: Optional[str] = None # 点位级协议覆盖;空 = 按 YAML device_prefixes 路由 row_number: int = 0 # CSV 行号(从 2 开始,表头为第 1 行),用于报错定位 @property def topic(self) -> str: """Kafka 上行 topic(模板化:按设备聚合)。""" return f"{self.device_id}.points" class PointDict: """点位字典内存模型。""" def __init__(self, points: List[Point]): self.points = points def by_point_id(self) -> Dict[str, Point]: return {p.point_id: p for p in self.points} def by_device_id(self) -> Dict[str, List[Point]]: grouped: Dict[str, List[Point]] = {} for p in self.points: grouped.setdefault(p.device_id, []).append(p) return grouped def __len__(self) -> int: return len(self.points) def _to_bool(raw: str) -> bool: """宽松解析布尔列(true/false/1/0/yes/no,大小写不敏感)。""" return raw.strip().lower() in ("1", "true", "yes", "y", "on") def load_point_dict_csv(path: str) -> PointDict: """从 CSV 文件加载点位字典。 Args: path: CSV 文件路径(UTF-8,含表头,表头列名对齐 schema.CSV_HEADERS)。 Returns: PointDict:点位内存模型。结构/取值问题不在此抛出, 统一由 validator.validate_point_dict() 报告(便于聚合展示全部错误)。 """ points: List[Point] = [] with open(path, "r", encoding="utf-8-sig") as fh: reader = csv.DictReader(fh) fieldnames = list(reader.fieldnames or []) for row_number, row in enumerate(reader, start=2): sample_rate_raw = (row.get("sampleRate") or "").strip() try: sample_rate = int(float(sample_rate_raw)) if sample_rate_raw else 0 except ValueError: sample_rate = 0 points.append( Point( device_id=(row.get("device_id") or "").strip(), point_id=(row.get("point_id") or "").strip(), name=(row.get("name") or "").strip(), unit=(row.get("unit") or "").strip(), data_type=(row.get("dataType") or "").strip(), sample_rate=sample_rate, quality_code=_to_bool(row["qualityCode"]) if (row.get("qualityCode") or "").strip() else True, opc_node=((row.get("opcNode") or "").strip() or None), protocol=((row.get("protocol") or "").strip().lower() or None), row_number=row_number, ) ) return PointDict(points)