Files

83 lines
4.4 KiB
Python

"""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")