"""Virtual farm nodes — replay the cleaned farm log to the API so the dashboard works before the hardware is deployed (and to load-test the server). python simulate_nodes.py # live: one reading per node every 5 s python simulate_nodes.py --backfill 72 # instantly insert the last 72 hours (timestamps shifted to now) Also executes pending pump/fan commands like a real ESP32 would (prints them and sends the ACK). """ import argparse import json import time import urllib.request from datetime import datetime, timedelta from pathlib import Path import pandas as pd import config BASE = Path(__file__).resolve().parent SRC = BASE.parent / "ai" / "outputs" / "node_log_clean.csv" RAW = BASE.parent.parent / "datasets" / "node_log_raw.csv" def call(url, payload=None): req = urllib.request.Request(url, data=json.dumps(payload).encode() if payload else None, headers={"X-API-Key": config.NODE_API_KEY, "Content-Type": "application/json"}, method="POST" if payload is not None else "GET") with urllib.request.urlopen(req, timeout=5) as r: return json.load(r) def load(): df = pd.read_csv(SRC if SRC.exists() else RAW, parse_dates=["timestamp"]) df = df.dropna(subset=["air_temp_c", "soil_moisture_pct"]) return df.sort_values("timestamp") def row_payload(r, ts): return {"node_id": r.node_id, "ts": ts.isoformat(timespec="seconds"), "air_temp_c": round(r.air_temp_c, 2), "air_humidity_pct": round(r.air_humidity_pct, 1), "soil_moisture_pct": round(r.soil_moisture_pct, 1), "light": int(r.light_adc), "battery_v": float(r.battery_v), "rssi": -67, "fw": "sim-1.0"} if __name__ == "__main__": ap = argparse.ArgumentParser() ap.add_argument("--url", default="http://127.0.0.1:5000") ap.add_argument("--backfill", type=int, default=0, help="hours of history to insert instantly") ap.add_argument("--interval", type=float, default=5.0) a = ap.parse_args() df = load() if a.backfill: last = df["timestamp"].max() part = df[df["timestamp"] > last - timedelta(hours=a.backfill)] shift = datetime.now() - last for r in part.itertuples(): call(a.url + "/api/v1/readings", row_payload(r, r.timestamp + shift)) print(f"inserted {len(part)} readings ({a.backfill} h)") else: groups = {n: g.reset_index(drop=True) for n, g in df.groupby("node_id")} i = 0 while True: for node, g in groups.items(): r = g.iloc[i % len(g)] call(a.url + "/api/v1/readings", row_payload(r, datetime.now())) for c in call(a.url + f"/api/v1/commands/pending?node_id={node}")["commands"]: print(f"[{node}] relay -> {c['actuator']} {c['action']} {c['duration_min']} min") call(a.url + f"/api/v1/commands/{c['id']}/ack", {}) i += 1 print(datetime.now().strftime("%H:%M:%S"), "sent", len(groups), "readings") time.sleep(a.interval)