Add desktop simulator
This commit is contained in:
@@ -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/<name>", 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)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
flask
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Xterra Dashboard Simulator</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Xterra Dashboard</h1>
|
||||
|
||||
<section class="grid">
|
||||
<div class="card">
|
||||
<h2>Battery</h2>
|
||||
<p><span id="soc">--</span>%</p>
|
||||
<small><span id="voltage">--</span>V / <span id="current">--</span>A</small>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Runtime</h2>
|
||||
<p><span id="runtime">--</span> hr</p>
|
||||
<small><span id="remaining">--</span> Ah remaining</small>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Fridge Zone 1</h2>
|
||||
<p><span id="fridge1">--</span>°F</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Fridge Zone 2</h2>
|
||||
<p><span id="fridge2">--</span>°F</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Rear Seat</h2>
|
||||
<p><span id="rear">--</span>°F</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Outside</h2>
|
||||
<p><span id="outside">--</span>°F</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="controls">
|
||||
<button onclick="toggleRelay('starlink')" id="starlinkBtn">Starlink</button>
|
||||
<button onclick="toggleRelay('fridge')" id="fridgeBtn">Fridge</button>
|
||||
<button onclick="enableWifi()">Enable WiFi 10 min</button>
|
||||
</section>
|
||||
|
||||
<section class="status">
|
||||
<p>RS-485: <span id="rs485">--</span></p>
|
||||
<p>WiFi: <span id="wifi">--</span></p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
let relayState = {};
|
||||
|
||||
async function loadStatus() {
|
||||
const res = await fetch('/status');
|
||||
const data = await res.json();
|
||||
|
||||
document.getElementById('soc').textContent = data.battery.soc;
|
||||
document.getElementById('voltage').textContent = data.battery.voltage;
|
||||
document.getElementById('current').textContent = data.battery.current;
|
||||
document.getElementById('runtime').textContent = data.battery.runtime_hours;
|
||||
document.getElementById('remaining').textContent = data.battery.remaining_ah;
|
||||
|
||||
document.getElementById('fridge1').textContent = data.temps.fridge_zone_1;
|
||||
document.getElementById('fridge2').textContent = data.temps.fridge_zone_2;
|
||||
document.getElementById('rear').textContent = data.temps.rear_seat;
|
||||
document.getElementById('outside').textContent = data.temps.outside;
|
||||
|
||||
relayState = data.relays;
|
||||
|
||||
document.getElementById('starlinkBtn').textContent =
|
||||
relayState.starlink ? 'Starlink: ON' : 'Starlink: OFF';
|
||||
|
||||
document.getElementById('fridgeBtn').textContent =
|
||||
relayState.fridge ? 'Fridge: ON' : 'Fridge: OFF';
|
||||
|
||||
document.getElementById('rs485').textContent =
|
||||
data.network.rs485_connected ? 'Connected' : 'Disconnected';
|
||||
|
||||
document.getElementById('wifi').textContent =
|
||||
data.network.wifi_enabled ? 'Enabled' : 'Disabled';
|
||||
}
|
||||
|
||||
async function toggleRelay(name) {
|
||||
await fetch(`/relay/${name}`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({state: !relayState[name]})
|
||||
});
|
||||
|
||||
loadStatus();
|
||||
}
|
||||
|
||||
async function enableWifi() {
|
||||
await fetch('/network/wifi', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({minutes: 10})
|
||||
});
|
||||
|
||||
loadStatus();
|
||||
}
|
||||
|
||||
loadStatus();
|
||||
setInterval(loadStatus, 2000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user