"""Pure, versioned generator. No clock, random process state, network or writes.""" from datetime import datetime, timedelta, timezone from hashlib import sha256 from uuid import NAMESPACE_URL, uuid5 from .schemas import DemoContent VERSION = "water-demo@2" DEFAULT_SEED = "tjwater-peripheral-demo-20260917" DEFAULT_START = datetime(2026, 9, 17, tzinfo=timezone.utc) def build_demo_content(seed: str = DEFAULT_SEED, start: datetime = DEFAULT_START) -> dict: """Same version/seed/UTC start produce identical canonical content and IDs.""" if not isinstance(seed, str) or not 1 <= len(seed) <= 128: raise ValueError("seed must contain 1 to 128 characters") if not isinstance(start, datetime) or start.tzinfo is None or start.utcoffset() is None: raise ValueError("start must include timezone") start = start.astimezone(timezone.utc) if start.second or start.microsecond or start.minute % 15: raise ValueError("start must be aligned to a 15-minute UTC boundary") prefix = f"{VERSION}/{seed}/{start.isoformat()}" def record_id(kind: str, key: str) -> str: return str(uuid5(NAMESPACE_URL, f"{prefix}/{kind}/{key}")) def at(index: int) -> str: return (start + timedelta(minutes=15 * index)).isoformat() points = [ ("DEMO-P001", "演示压力测点 1", "m", 24.37), ("DEMO-P002", "演示压力测点 2", "m", 31.26), ("DEMO-P003", "演示压力测点 3", "m", 31.81), ("DEMO-F001", "演示出水流量", "m3/h", 186.43), ("DEMO-L001", "演示水池液位", "m", 4.26), ] observations = [] for index in range(48): for point_id, _label, unit, base in points: noise = int(sha256(f"{prefix}/{point_id}/{index}".encode()).hexdigest()[:8], 16) % 101 value = round(base + (noise - 50) / 100, 2) quality = "valid" if point_id == "DEMO-P001" and 12 <= index < 16: value, quality = None, "missing" elif point_id == "DEMO-F001" and 24 <= index < 28: value = 0.0 elif point_id == "DEMO-L001" and 36 <= index < 40: value = round(1.35 + noise / 1000, 3) observations.append({"record_id": record_id("observation", f"{point_id}/{index}"), "point_id": point_id, "observed_at": at(index), "value": value, "quality": quality, "unit": unit}) states = [] for asset, kind, changes in ( ("DEMO-P001", "pressure_sensor", [(0, "online"), (12, "offline"), (16, "online")]), ("DEMO-PUMP-01", "pump", [(0, "running"), (24, "stopped"), (28, "running")]), ("DEMO-VALVE-01", "valve", [(0, "open"), (24, "closed"), (28, "open")]), ): for index, state in changes: states.append({"record_id": record_id("state", f"{asset}/{index}"), "asset_id": asset, "asset_type": kind, "observed_at": at(index), "state": state}) states.sort(key=lambda record: (record["observed_at"], record["asset_id"])) alerts = [ {"alert_id": record_id("alert", "communication"), "asset_id": "DEMO-P001", "occurred_at": at(12), "cleared_at": at(16), "code": "communication_lost", "message": "预置通信中断告警样例,仅为合成情节,不是现场诊断"}, {"alert_id": record_id("alert", "level"), "asset_id": "DEMO-L001", "occurred_at": at(36), "cleared_at": at(40), "code": "low_level", "message": "预置低液位告警样例,不采用生产阈值或专业告警算法"}, ] operations = [ {"record_id": record_id("operation", operation), "asset_id": asset, "occurred_at": at(index), "operation": operation, "actor": "synthetic-generator", "note": "合成运行历史,不是设备指令或实际操作回执"} for asset, index, operation in (("DEMO-PUMP-01", 24, "pump_stop"), ("DEMO-VALVE-01", 24, "valve_close"), ("DEMO-PUMP-01", 28, "pump_start"), ("DEMO-VALVE-01", 28, "valve_open")) ] content = {"generator_version": VERSION, "seed": seed, "origin": "synthetic", "window": {"start": at(0), "end": at(48)}, "points": [{"point_id": point, "label": label, "unit": unit, "status": "unknown", "origin": "synthetic"} for point, label, unit, _ in points], "observations": observations, "device_states": states, "alerts": alerts, "operations": operations} return DemoContent.model_validate(content).model_dump(mode="json")