Add manual AP mode override via web UI

- State Machine prüft /tmp/babycam-ap-override: 'on'/'off' = manuell, fehlt = Automatik
- Neue API-Endpunkte: POST /ap/on, /ap/off, /ap/auto
- Access-Point-Karte zeigt jetzt 3 Buttons (An / Aus / Auto) + aktuellen Modus
- Manuell überschreibt die Automatik, Auto gibt Kontrolle zurück an die State Machine

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Julian Vollmer 2026-05-18 15:54:53 +02:00
parent d9514e23d7
commit 7cfc958cf0
3 changed files with 97 additions and 19 deletions

View File

@ -10,6 +10,7 @@ import subprocess
import json
import re
import time
import os
from flask import Flask, Response, render_template, request, jsonify
app = Flask(__name__)
@ -113,6 +114,7 @@ def get_system_status():
"ips": ips,
"wifi_ssid": wifi_ssid,
"ap_active": ap_active,
"ap_override": get_ap_override(),
"cam_available": cam_available,
"uptime": _get_uptime(),
}
@ -159,6 +161,34 @@ def wifi_scan():
return jsonify(get_wifi_networks())
@app.route("/ap/<action>", methods=["POST"])
def ap_control(action):
override_file = "/tmp/babycam-ap-override"
if action == "on":
with open(override_file, "w") as f:
f.write("on")
return jsonify({"success": True, "mode": "on"})
elif action == "off":
with open(override_file, "w") as f:
f.write("off")
return jsonify({"success": True, "mode": "off"})
elif action == "auto":
try:
os.remove(override_file)
except FileNotFoundError:
pass
return jsonify({"success": True, "mode": "auto"})
return jsonify({"success": False, "error": "Unbekannte Aktion"}), 400
def get_ap_override():
try:
with open("/tmp/babycam-ap-override") as f:
return f.read().strip().lower()
except FileNotFoundError:
return "auto"
@app.route("/wifi/connect", methods=["POST"])
def wifi_connect():
data = request.get_json()

View File

@ -219,10 +219,18 @@
</div>
<div class="card">
<label>Access Point</label>
<div class="value">
<div class="value" style="display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;">
<span class="badge {{ 'on' if status.ap_active else 'off' }}">
{{ "aktiv" if status.ap_active else "aus" }}
</span>
<span style="font-size:.75rem;color:#666;">
{{ {"on":"manuell an","off":"manuell aus","auto":"automatisch"}[status.ap_override] }}
</span>
</div>
<div style="display:flex;gap:.4rem;margin-top:.6rem;flex-wrap:wrap;">
<button onclick="setAP('on')" class="{{ 'primary' if status.ap_override == 'on' else '' }}" style="font-size:.75rem;padding:.3rem .7rem;">An</button>
<button onclick="setAP('off')" class="{{ 'primary' if status.ap_override == 'off' else '' }}" style="font-size:.75rem;padding:.3rem .7rem;">Aus</button>
<button onclick="setAP('auto')"class="{{ 'primary' if status.ap_override == 'auto' else '' }}" style="font-size:.75rem;padding:.3rem .7rem;">Auto</button>
</div>
</div>
<div class="card">
@ -281,6 +289,12 @@
document.getElementById("wifi-password").focus();
}
async function setAP(action) {
const res = await fetch(`/ap/${action}`, { method: "POST" });
const data = await res.json();
if (data.success) setTimeout(() => location.reload(), 800);
}
async function connectWifi() {
if (!selectedSSID) return;
const password = document.getElementById("wifi-password").value;

View File

@ -2,15 +2,21 @@
"""
Babycam Network State Machine
Verwaltet die drei Betriebsmodi:
A Heimnetz (wlan0 verbunden)
B Kein WLAN (AP auf wlan1)
A Heimnetz (wlan0 verbunden, AP aus)
B Kein WLAN (AP auf wlan1 automatisch)
C Setup-Modus (AP läuft, User konfiguriert WLAN)
Override-Datei: /tmp/babycam-ap-override
"on" AP immer an (manuell)
"off" AP immer aus (manuell)
fehlt Automatik
"""
import subprocess
import time
import logging
import sys
import os
logging.basicConfig(
level=logging.INFO,
@ -23,21 +29,30 @@ AP_INTERFACE = "wlan1"
CLIENT_INTERFACE = "wlan0"
AP_IP = "192.168.50.1"
CHECK_INTERVAL = 30 # Sekunden
OVERRIDE_FILE = "/tmp/babycam-ap-override"
def run(cmd, check=False):
result = subprocess.run(
cmd, shell=True, capture_output=True, text=True
)
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
if check and result.returncode != 0:
log.error(f"Fehler bei: {cmd}\n{result.stderr}")
return result
def get_override():
"""Liest die manuelle Override-Einstellung. Gibt 'on', 'off' oder None zurück."""
try:
with open(OVERRIDE_FILE) as f:
val = f.read().strip().lower()
if val in ("on", "off"):
return val
except FileNotFoundError:
pass
return None
def is_wlan0_connected():
result = run(
f"nmcli -t -f GENERAL.STATE device show {CLIENT_INTERFACE}"
)
result = run(f"nmcli -t -f GENERAL.STATE device show {CLIENT_INTERFACE}")
return "100 (connected)" in result.stdout
@ -68,12 +83,31 @@ def main():
current_mode = None
while True:
override = get_override()
connected = is_wlan0_connected()
ap_running = is_ap_running()
if override == "on":
# Manuell: AP immer an
if current_mode != "MANUAL_ON":
log.info("Modus: Manuell Access Point erzwungen AN.")
if not ap_running:
start_ap()
current_mode = "MANUAL_ON"
elif override == "off":
# Manuell: AP immer aus
if current_mode != "MANUAL_OFF":
log.info("Modus: Manuell Access Point erzwungen AUS.")
if ap_running:
stop_ap()
current_mode = "MANUAL_OFF"
else:
# Automatik
if connected:
if current_mode != "A":
log.info("Modus A: Heimnetz aktiv.")
log.info("Modus A: Heimnetz aktiv AP aus.")
if ap_running:
stop_ap()
current_mode = "A"