Add sensor fault simulation and health display
This commit is contained in:
@@ -68,5 +68,19 @@ def toggle_ignition():
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/sensor/<name>/fault", methods=["POST"])
|
||||||
|
def toggle_sensor_fault(name):
|
||||||
|
failed = esp32.toggle_sensor_fault(name)
|
||||||
|
|
||||||
|
if failed is None:
|
||||||
|
return jsonify({"success": False, "error": "Unknown sensor"}), 404
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"sensor": name,
|
||||||
|
"failed": failed
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0", port=5000, debug=True)
|
app.run(host="0.0.0.0", port=5000, debug=True)
|
||||||
|
|||||||
+32
-8
@@ -13,6 +13,7 @@ class ESP32Simulator:
|
|||||||
|
|
||||||
self.wifi_override_until = 0
|
self.wifi_override_until = 0
|
||||||
self.ignition_on = True
|
self.ignition_on = True
|
||||||
|
self.failed_sensors = set()
|
||||||
self.soc = 82.0
|
self.soc = 82.0
|
||||||
self.last_update = time.time()
|
self.last_update = time.time()
|
||||||
|
|
||||||
@@ -43,18 +44,23 @@ class ESP32Simulator:
|
|||||||
starlink_on = self.relays["starlink"]
|
starlink_on = self.relays["starlink"]
|
||||||
wifi_override_active = time.time() < self.wifi_override_until
|
wifi_override_active = time.time() < self.wifi_override_until
|
||||||
|
|
||||||
|
sensor_names = [
|
||||||
|
"fridge_zone_1",
|
||||||
|
"fridge_zone_2",
|
||||||
|
"rear_seat",
|
||||||
|
"outside"
|
||||||
|
]
|
||||||
|
|
||||||
temps = {
|
temps = {
|
||||||
"fridge_zone_1": self.sensor(36, 2),
|
"fridge_zone_1": None if "fridge_zone_1" in self.failed_sensors else self.sensor(36, 2),
|
||||||
"fridge_zone_2": self.sensor(12, 3),
|
"fridge_zone_2": None if "fridge_zone_2" in self.failed_sensors else self.sensor(12, 3),
|
||||||
"rear_seat": self.sensor(78, 8),
|
"rear_seat": None if "rear_seat" in self.failed_sensors else self.sensor(78, 8),
|
||||||
"outside": self.sensor(88, 8)
|
"outside": None if "outside" in self.failed_sensors else self.sensor(88, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
sensor_health = {
|
sensor_health = {
|
||||||
"fridge_zone_1": True,
|
name: name not in self.failed_sensors
|
||||||
"fridge_zone_2": True,
|
for name in sensor_names
|
||||||
"rear_seat": True,
|
|
||||||
"outside": True
|
|
||||||
}
|
}
|
||||||
|
|
||||||
remaining_ah = round(self.soc, 1)
|
remaining_ah = round(self.soc, 1)
|
||||||
@@ -108,5 +114,23 @@ class ESP32Simulator:
|
|||||||
self.ignition_on = not self.ignition_on
|
self.ignition_on = not self.ignition_on
|
||||||
return self.ignition_on
|
return self.ignition_on
|
||||||
|
|
||||||
|
def toggle_sensor_fault(self, name):
|
||||||
|
valid = {
|
||||||
|
"fridge_zone_1",
|
||||||
|
"fridge_zone_2",
|
||||||
|
"rear_seat",
|
||||||
|
"outside"
|
||||||
|
}
|
||||||
|
|
||||||
|
if name not in valid:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if name in self.failed_sensors:
|
||||||
|
self.failed_sensors.remove(name)
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.failed_sensors.add(name)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
esp32 = ESP32Simulator()
|
esp32 = ESP32Simulator()
|
||||||
|
|||||||
@@ -292,3 +292,19 @@ nav button {
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.sensor-fault {
|
||||||
|
border-color: #b00020;
|
||||||
|
background: #2a1010;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sensor-fault p {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #ffdddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sensor-fault small {
|
||||||
|
color: #ffaaaa;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,14 +35,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="card"><h2>Fridge A</h2><p><span id="dashFridge1">--</span>°F</p></div>
|
<div class="card" id="cardFridge1"><h2>Fridge A</h2><p><span id="dashFridge1">--</span></p><small id="healthFridge1">OK</small></div>
|
||||||
<div class="card"><h2>Fridge B</h2><p><span id="dashFridge2">--</span>°F</p></div>
|
<div class="card" id="cardFridge2"><h2>Fridge B</h2><p><span id="dashFridge2">--</span></p><small id="healthFridge2">OK</small></div>
|
||||||
<div class="card kid-card">
|
<div class="card kid-card" id="cardRear">
|
||||||
<h2>👧 Kid Area</h2>
|
<h2>👧 Kid Area</h2>
|
||||||
<p><span id="dashRear">--</span>°F</p>
|
<p><span id="dashRear">--</span></p>
|
||||||
<small id="kidStatus">Normal</small>
|
<small id="kidStatus">Normal</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="card"><h2>Outside</h2><p><span id="dashOutside">--</span>°F</p></div>
|
<div class="card" id="cardOutside"><h2>Outside</h2><p><span id="dashOutside">--</span></p><small id="healthOutside">OK</small></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relay-row">
|
<div class="relay-row">
|
||||||
@@ -66,10 +66,10 @@
|
|||||||
<section id="temps" class="screen">
|
<section id="temps" class="screen">
|
||||||
<h2>Temperatures</h2>
|
<h2>Temperatures</h2>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="card"><h2>Fridge Zone 1</h2><p><span id="tempFridge1">--</span>°F</p></div>
|
<div class="card"><h2>Fridge Zone 1</h2><p><span id="tempFridge1">--</span></p></div>
|
||||||
<div class="card"><h2>Fridge Zone 2</h2><p><span id="tempFridge2">--</span>°F</p></div>
|
<div class="card"><h2>Fridge Zone 2</h2><p><span id="tempFridge2">--</span></p></div>
|
||||||
<div class="card"><h2>Rear Seat</h2><p><span id="tempRear">--</span>°F</p></div>
|
<div class="card"><h2>Rear Seat</h2><p><span id="tempRear">--</span></p></div>
|
||||||
<div class="card"><h2>Outside</h2><p><span id="tempOutside">--</span>°F</p></div>
|
<div class="card"><h2>Outside</h2><p><span id="tempOutside">--</span></p></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -90,6 +90,14 @@
|
|||||||
<button onclick="enableWifi()">Enable WiFi 10 min</button>
|
<button onclick="enableWifi()">Enable WiFi 10 min</button>
|
||||||
<button onclick="toggleIgnition()">Toggle Ignition</button>
|
<button onclick="toggleIgnition()">Toggle Ignition</button>
|
||||||
|
|
||||||
|
<h2>Sensor Fault Simulation</h2>
|
||||||
|
<div class="settings-list">
|
||||||
|
<button onclick="toggleSensorFault('fridge_zone_1')">Toggle Fridge Zone 1 Fault</button>
|
||||||
|
<button onclick="toggleSensorFault('fridge_zone_2')">Toggle Fridge Zone 2 Fault</button>
|
||||||
|
<button onclick="toggleSensorFault('rear_seat')">Toggle Rear Seat Fault</button>
|
||||||
|
<button onclick="toggleSensorFault('outside')">Toggle Outside Fault</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2>Alarm Settings</h2>
|
<h2>Alarm Settings</h2>
|
||||||
<div class="settings-list">
|
<div class="settings-list">
|
||||||
<label>
|
<label>
|
||||||
@@ -155,25 +163,50 @@ function onOff(value) {
|
|||||||
return value ? 'ON' : 'OFF';
|
return value ? 'ON' : 'OFF';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function tempText(value) {
|
||||||
|
return value === null || value === undefined ? 'FAULT' : `${value}°F`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSensorCard(cardId, healthId, healthy) {
|
||||||
|
const card = document.getElementById(cardId);
|
||||||
|
const health = document.getElementById(healthId);
|
||||||
|
|
||||||
|
if (!card || !health) return;
|
||||||
|
|
||||||
|
if (healthy) {
|
||||||
|
card.classList.remove('sensor-fault');
|
||||||
|
health.textContent = 'OK';
|
||||||
|
} else {
|
||||||
|
card.classList.add('sensor-fault');
|
||||||
|
health.textContent = 'SENSOR FAULT';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function checkAlarms(data) {
|
function checkAlarms(data) {
|
||||||
const alarms = [];
|
const alarms = [];
|
||||||
const warnings = [];
|
const warnings = [];
|
||||||
|
|
||||||
if (data.temps.rear_seat >= alarmConfig.rear_seat_critical) {
|
for (const [name, healthy] of Object.entries(data.sensor_health)) {
|
||||||
|
if (!healthy) {
|
||||||
|
warnings.push(`Sensor fault: ${name.replaceAll('_', ' ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.sensor_health.rear_seat && data.temps.rear_seat >= alarmConfig.rear_seat_critical) {
|
||||||
alarms.push({
|
alarms.push({
|
||||||
key: 'rear_seat_critical',
|
key: 'rear_seat_critical',
|
||||||
title: 'REAR SEAT TEMP CRITICAL',
|
title: 'REAR SEAT TEMP CRITICAL',
|
||||||
message: `${data.temps.rear_seat}°F detected near car seat area`
|
message: `${data.temps.rear_seat}°F detected near car seat area`
|
||||||
});
|
});
|
||||||
} else if (data.temps.rear_seat >= alarmConfig.rear_seat_warning) {
|
} else if (data.sensor_health.rear_seat && data.temps.rear_seat >= alarmConfig.rear_seat_warning) {
|
||||||
warnings.push(`Rear seat temp high: ${data.temps.rear_seat}°F`);
|
warnings.push(`Rear seat temp high: ${data.temps.rear_seat}°F`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.temps.fridge_zone_1 >= alarmConfig.fridge_zone_1_warm) {
|
if (data.sensor_health.fridge_zone_1 && data.temps.fridge_zone_1 >= alarmConfig.fridge_zone_1_warm) {
|
||||||
warnings.push(`Fridge Zone 1 warm: ${data.temps.fridge_zone_1}°F`);
|
warnings.push(`Fridge Zone 1 warm: ${data.temps.fridge_zone_1}°F`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.temps.fridge_zone_2 >= alarmConfig.fridge_zone_2_warm) {
|
if (data.sensor_health.fridge_zone_2 && data.temps.fridge_zone_2 >= alarmConfig.fridge_zone_2_warm) {
|
||||||
warnings.push(`Fridge Zone 2 warm: ${data.temps.fridge_zone_2}°F`);
|
warnings.push(`Fridge Zone 2 warm: ${data.temps.fridge_zone_2}°F`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,19 +293,24 @@ async function loadStatus() {
|
|||||||
|
|
||||||
document.getElementById('dashSoc').textContent = data.battery.soc;
|
document.getElementById('dashSoc').textContent = data.battery.soc;
|
||||||
document.getElementById('dashRuntime').textContent = data.battery.runtime_hours;
|
document.getElementById('dashRuntime').textContent = data.battery.runtime_hours;
|
||||||
document.getElementById('dashFridge1').textContent = data.temps.fridge_zone_1;
|
document.getElementById('dashFridge1').textContent = tempText(data.temps.fridge_zone_1);
|
||||||
document.getElementById('dashFridge2').textContent = data.temps.fridge_zone_2;
|
document.getElementById('dashFridge2').textContent = tempText(data.temps.fridge_zone_2);
|
||||||
document.getElementById('dashRear').textContent = data.temps.rear_seat;
|
document.getElementById('dashRear').textContent = tempText(data.temps.rear_seat);
|
||||||
|
|
||||||
|
setSensorCard('cardFridge1', 'healthFridge1', data.sensor_health.fridge_zone_1);
|
||||||
|
setSensorCard('cardFridge2', 'healthFridge2', data.sensor_health.fridge_zone_2);
|
||||||
|
setSensorCard('cardRear', 'kidStatus', data.sensor_health.rear_seat);
|
||||||
|
setSensorCard('cardOutside', 'healthOutside', data.sensor_health.outside);
|
||||||
|
|
||||||
const kidStatus = document.getElementById('kidStatus');
|
const kidStatus = document.getElementById('kidStatus');
|
||||||
if (data.temps.rear_seat >= alarmConfig.rear_seat_critical) {
|
if (data.sensor_health.rear_seat && data.temps.rear_seat >= alarmConfig.rear_seat_critical) {
|
||||||
kidStatus.textContent = 'CRITICAL';
|
kidStatus.textContent = 'CRITICAL';
|
||||||
} else if (data.temps.rear_seat >= alarmConfig.rear_seat_warning) {
|
} else if (data.sensor_health.rear_seat && data.temps.rear_seat >= alarmConfig.rear_seat_warning) {
|
||||||
kidStatus.textContent = 'Warning';
|
kidStatus.textContent = 'Warning';
|
||||||
} else {
|
} else {
|
||||||
kidStatus.textContent = 'Normal';
|
kidStatus.textContent = 'Normal';
|
||||||
}
|
}
|
||||||
document.getElementById('dashOutside').textContent = data.temps.outside;
|
document.getElementById('dashOutside').textContent = tempText(data.temps.outside);
|
||||||
document.getElementById('dashStarlink').textContent = `Starlink: ${onOff(data.relays.starlink)}`;
|
document.getElementById('dashStarlink').textContent = `Starlink: ${onOff(data.relays.starlink)}`;
|
||||||
document.getElementById('dashFridgeRelay').textContent = `Fridge: ${onOff(data.relays.fridge)}`;
|
document.getElementById('dashFridgeRelay').textContent = `Fridge: ${onOff(data.relays.fridge)}`;
|
||||||
|
|
||||||
@@ -283,10 +321,10 @@ async function loadStatus() {
|
|||||||
document.getElementById('batRuntime').textContent = data.battery.runtime_hours;
|
document.getElementById('batRuntime').textContent = data.battery.runtime_hours;
|
||||||
document.getElementById('batTemp').textContent = data.battery.temperature_f;
|
document.getElementById('batTemp').textContent = data.battery.temperature_f;
|
||||||
|
|
||||||
document.getElementById('tempFridge1').textContent = data.temps.fridge_zone_1;
|
document.getElementById('tempFridge1').textContent = tempText(data.temps.fridge_zone_1);
|
||||||
document.getElementById('tempFridge2').textContent = data.temps.fridge_zone_2;
|
document.getElementById('tempFridge2').textContent = tempText(data.temps.fridge_zone_2);
|
||||||
document.getElementById('tempRear').textContent = data.temps.rear_seat;
|
document.getElementById('tempRear').textContent = tempText(data.temps.rear_seat);
|
||||||
document.getElementById('tempOutside').textContent = data.temps.outside;
|
document.getElementById('tempOutside').textContent = tempText(data.temps.outside);
|
||||||
|
|
||||||
document.getElementById('starlinkBtn').textContent = `Starlink: ${onOff(data.relays.starlink)}`;
|
document.getElementById('starlinkBtn').textContent = `Starlink: ${onOff(data.relays.starlink)}`;
|
||||||
document.getElementById('fridgeBtn').textContent = `Fridge: ${onOff(data.relays.fridge)}`;
|
document.getElementById('fridgeBtn').textContent = `Fridge: ${onOff(data.relays.fridge)}`;
|
||||||
@@ -347,6 +385,17 @@ async function toggleIgnition() {
|
|||||||
loadStatus();
|
loadStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleSensorFault(name) {
|
||||||
|
await fetch(`/sensor/${name}/fault`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({})
|
||||||
|
});
|
||||||
|
|
||||||
|
acknowledgedAlarms.clear();
|
||||||
|
loadStatus();
|
||||||
|
}
|
||||||
|
|
||||||
async function enableWifi() {
|
async function enableWifi() {
|
||||||
await fetch('/network/wifi', {
|
await fetch('/network/wifi', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
Reference in New Issue
Block a user