Files
next-tjwater-server/samples/peripheral/demo_data.py
T

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