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