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 json
import re import re
import time import time
import os
from flask import Flask, Response, render_template, request, jsonify from flask import Flask, Response, render_template, request, jsonify
app = Flask(__name__) app = Flask(__name__)
@ -113,6 +114,7 @@ def get_system_status():
"ips": ips, "ips": ips,
"wifi_ssid": wifi_ssid, "wifi_ssid": wifi_ssid,
"ap_active": ap_active, "ap_active": ap_active,
"ap_override": get_ap_override(),
"cam_available": cam_available, "cam_available": cam_available,
"uptime": _get_uptime(), "uptime": _get_uptime(),
} }
@ -159,6 +161,34 @@ def wifi_scan():
return jsonify(get_wifi_networks()) 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"]) @app.route("/wifi/connect", methods=["POST"])
def wifi_connect(): def wifi_connect():
data = request.get_json() data = request.get_json()

View File

@ -219,10 +219,18 @@
</div> </div>
<div class="card"> <div class="card">
<label>Access Point</label> <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' }}"> <span class="badge {{ 'on' if status.ap_active else 'off' }}">
{{ "aktiv" if status.ap_active else "aus" }} {{ "aktiv" if status.ap_active else "aus" }}
</span> </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> </div>
<div class="card"> <div class="card">
@ -281,6 +289,12 @@
document.getElementById("wifi-password").focus(); 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() { async function connectWifi() {
if (!selectedSSID) return; if (!selectedSSID) return;
const password = document.getElementById("wifi-password").value; const password = document.getElementById("wifi-password").value;

View File

@ -2,15 +2,21 @@
""" """
Babycam Network State Machine Babycam Network State Machine
Verwaltet die drei Betriebsmodi: Verwaltet die drei Betriebsmodi:
A Heimnetz (wlan0 verbunden) A Heimnetz (wlan0 verbunden, AP aus)
B Kein WLAN (AP auf wlan1) B Kein WLAN (AP auf wlan1 automatisch)
C Setup-Modus (AP läuft, User konfiguriert WLAN) 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 subprocess
import time import time
import logging import logging
import sys import sys
import os
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
@ -23,21 +29,30 @@ AP_INTERFACE = "wlan1"
CLIENT_INTERFACE = "wlan0" CLIENT_INTERFACE = "wlan0"
AP_IP = "192.168.50.1" AP_IP = "192.168.50.1"
CHECK_INTERVAL = 30 # Sekunden CHECK_INTERVAL = 30 # Sekunden
OVERRIDE_FILE = "/tmp/babycam-ap-override"
def run(cmd, check=False): def run(cmd, check=False):
result = subprocess.run( result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
cmd, shell=True, capture_output=True, text=True
)
if check and result.returncode != 0: if check and result.returncode != 0:
log.error(f"Fehler bei: {cmd}\n{result.stderr}") log.error(f"Fehler bei: {cmd}\n{result.stderr}")
return result 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(): def is_wlan0_connected():
result = run( result = run(f"nmcli -t -f GENERAL.STATE device show {CLIENT_INTERFACE}")
f"nmcli -t -f GENERAL.STATE device show {CLIENT_INTERFACE}"
)
return "100 (connected)" in result.stdout return "100 (connected)" in result.stdout
@ -68,12 +83,31 @@ def main():
current_mode = None current_mode = None
while True: while True:
override = get_override()
connected = is_wlan0_connected() connected = is_wlan0_connected()
ap_running = is_ap_running() 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 connected:
if current_mode != "A": if current_mode != "A":
log.info("Modus A: Heimnetz aktiv.") log.info("Modus A: Heimnetz aktiv AP aus.")
if ap_running: if ap_running:
stop_ap() stop_ap()
current_mode = "A" current_mode = "A"