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,21 +83,40 @@ 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 connected: if override == "on":
if current_mode != "A": # Manuell: AP immer an
log.info("Modus A: Heimnetz aktiv.") if current_mode != "MANUAL_ON":
if ap_running: log.info("Modus: Manuell – Access Point erzwungen AN.")
stop_ap()
current_mode = "A"
else:
if current_mode != "B":
log.info("Modus B: Kein WLAN – starte Access Point.")
if not ap_running: if not ap_running:
start_ap() start_ap()
current_mode = "B" 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 – AP aus.")
if ap_running:
stop_ap()
current_mode = "A"
else:
if current_mode != "B":
log.info("Modus B: Kein WLAN – starte Access Point.")
if not ap_running:
start_ap()
current_mode = "B"
time.sleep(CHECK_INTERVAL) time.sleep(CHECK_INTERVAL)