diff --git a/simulator/app.py b/simulator/app.py index e69de29..99c58c4 100644 --- a/simulator/app.py +++ b/simulator/app.py @@ -0,0 +1,107 @@ +from flask import Flask, jsonify, render_template, request +import random +import time + +app = Flask(__name__) + +state = { + "relays": { + "starlink": False, + "fridge": True + }, + "wifi_override_until": 0 +} + + +def fake_status(): + starlink_on = state["relays"]["starlink"] + wifi_override_active = time.time() < state["wifi_override_until"] + + return { + "timestamp": int(time.time()), + + "battery": { + "soc": random.randint(76, 84), + "voltage": round(random.uniform(13.0, 13.4), 2), + "current": round(random.uniform(-8.0, -3.0), 1), + "remaining_ah": round(random.uniform(76.0, 84.0), 1), + "runtime_hours": round(random.uniform(9.5, 14.0), 1), + "temperature_f": round(random.uniform(70.0, 82.0), 1) + }, + + "temps": { + "fridge_zone_1": round(random.uniform(34.0, 38.0), 1), + "fridge_zone_2": round(random.uniform(8.0, 16.0), 1), + "rear_seat": round(random.uniform(72.0, 86.0), 1), + "outside": round(random.uniform(80.0, 96.0), 1) + }, + + "relays": state["relays"], + + "network": { + "wifi_enabled": starlink_on or wifi_override_active, + "wifi_override_active": wifi_override_active, + "rs485_connected": True, + "starlink_enabled": starlink_on + } + } + + +@app.route("/") +def index(): + return render_template("index.html") + + +@app.route("/status") +def status(): + return jsonify(fake_status()) + + +@app.route("/battery") +def battery(): + return jsonify(fake_status()["battery"]) + + +@app.route("/temps") +def temps(): + return jsonify(fake_status()["temps"]) + + +@app.route("/relays") +def relays(): + return jsonify(state["relays"]) + + +@app.route("/relay/", methods=["POST"]) +def set_relay(name): + if name not in state["relays"]: + return jsonify({"success": False, "error": "Unknown relay"}), 404 + + data = request.get_json(force=True) + state["relays"][name] = bool(data.get("state", False)) + + return jsonify({ + "success": True, + name: state["relays"][name] + }) + + +@app.route("/network") +def network(): + return jsonify(fake_status()["network"]) + + +@app.route("/network/wifi", methods=["POST"]) +def enable_wifi(): + data = request.get_json(force=True) + minutes = int(data.get("minutes", 10)) + state["wifi_override_until"] = time.time() + minutes * 60 + + return jsonify({ + "success": True, + "expires_minutes": minutes + }) + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000, debug=True) diff --git a/simulator/requirements.txt b/simulator/requirements.txt index e69de29..7e10602 100644 --- a/simulator/requirements.txt +++ b/simulator/requirements.txt @@ -0,0 +1 @@ +flask diff --git a/simulator/static/style.css b/simulator/static/style.css new file mode 100644 index 0000000..ab265d9 --- /dev/null +++ b/simulator/static/style.css @@ -0,0 +1,69 @@ +body { + margin: 0; + font-family: Arial, sans-serif; + background: #111; + color: #eee; +} + +main { + max-width: 900px; + margin: 0 auto; + padding: 24px; +} + +h1 { + text-align: center; +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 16px; +} + +.card { + background: #222; + border: 1px solid #333; + border-radius: 12px; + padding: 18px; + text-align: center; +} + +.card h2 { + font-size: 1rem; + margin: 0 0 12px; + color: #aaa; +} + +.card p { + font-size: 2rem; + margin: 0; +} + +.controls { + margin-top: 24px; + display: flex; + gap: 12px; + flex-wrap: wrap; + justify-content: center; +} + +button { + font-size: 1rem; + padding: 14px 20px; + border: none; + border-radius: 10px; + background: #444; + color: white; + cursor: pointer; +} + +button:hover { + background: #666; +} + +.status { + margin-top: 24px; + text-align: center; + color: #aaa; +} diff --git a/simulator/templates/index.html b/simulator/templates/index.html new file mode 100644 index 0000000..b1f4d22 --- /dev/null +++ b/simulator/templates/index.html @@ -0,0 +1,114 @@ + + + + Xterra Dashboard Simulator + + + +
+

Xterra Dashboard

+ +
+
+

Battery

+

--%

+ --V / --A +
+ +
+

Runtime

+

-- hr

+ -- Ah remaining +
+ +
+

Fridge Zone 1

+

--°F

+
+ +
+

Fridge Zone 2

+

--°F

+
+ +
+

Rear Seat

+

--°F

+
+ +
+

Outside

+

--°F

+
+
+ +
+ + + +
+ +
+

RS-485: --

+

WiFi: --

+
+
+ + + +