83 lines
4.4 KiB
Python
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")
|