GUI verbessert und Admin console eingebaut

This commit is contained in:
2026-05-19 08:23:26 +02:00
parent bc542bcad6
commit 3f6a10bc79
8 changed files with 1302 additions and 115 deletions
@@ -0,0 +1,13 @@
[Unit]
Description=ExoMy Admin API
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/python3 /home/pi/ExoMy_Software/scripts/exomy_admin_api.py
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,194 @@
#!/usr/bin/env python3
import http.server
import json
import shutil
import socketserver
import subprocess
import time
PORT = 8082
EXOMY_CONTAINER = 'exomy_autostart'
class ReusableThreadingTCPServer(socketserver.ThreadingTCPServer):
allow_reuse_address = True
def run_command(command):
result = subprocess.run(command, capture_output=True, text=True, check=False)
return result.stdout.strip(), result.stderr.strip(), result.returncode
def read_cpu_temperature():
try:
with open('/sys/class/thermal/thermal_zone0/temp', 'r', encoding='utf-8') as handle:
raw_value = handle.read().strip()
return f'{int(raw_value) / 1000:.1f} °C'
except (FileNotFoundError, ValueError):
return 'unbekannt'
def format_uptime():
try:
with open('/proc/uptime', 'r', encoding='utf-8') as handle:
uptime_seconds = int(float(handle.read().split()[0]))
except (FileNotFoundError, ValueError, IndexError):
return 'unbekannt'
days, remainder = divmod(uptime_seconds, 86400)
hours, remainder = divmod(remainder, 3600)
minutes, _ = divmod(remainder, 60)
parts = []
if days:
parts.append(f'{days}d')
if days or hours:
parts.append(f'{hours}h')
parts.append(f'{minutes}m')
return ' '.join(parts)
def format_disk_free():
usage = shutil.disk_usage('/')
free_gb = usage.free / (1024 ** 3)
total_gb = usage.total / (1024 ** 3)
return f'{free_gb:.1f} GB frei / {total_gb:.1f} GB'
def get_ip_addresses():
stdout, _, returncode = run_command(['hostname', '-I'])
if returncode != 0 or not stdout:
return 'unbekannt'
ips = [item for item in stdout.split() if item]
return ', '.join(ips) if ips else 'unbekannt'
def get_wifi_status():
stdout, _, returncode = run_command(['nmcli', '-t', '-f', 'DEVICE,STATE,CONNECTION', 'device'])
if returncode != 0 or not stdout:
return 'unbekannt'
for line in stdout.splitlines():
parts = line.split(':', 2)
if len(parts) != 3:
continue
device, state, connection = parts
if device == 'wlan0':
if state == 'connected':
return f'verbunden: {connection}'
return state
return 'kein wlan0'
def get_systemd_state(service_name):
stdout, _, returncode = run_command(['systemctl', 'is-active', service_name])
if returncode == 0 and stdout:
return stdout
return stdout or 'unknown'
def get_container_state(container_name):
stdout, _, returncode = run_command(['docker', 'inspect', '-f', '{{.State.Status}}', container_name])
if returncode == 0 and stdout:
return stdout
return 'unknown'
def collect_status():
return {
'status': 'Bereit',
'system': {
'wifi': get_wifi_status(),
'ips': get_ip_addresses(),
'cpu_temperature': read_cpu_temperature(),
'uptime': format_uptime(),
'disk_free': format_disk_free(),
},
'services': {
'camera': get_systemd_state('exomy-camera-stream.service'),
'admin_api': get_systemd_state('exomy-admin-api.service'),
'exomy': get_container_state(EXOMY_CONTAINER),
}
}
def restart_camera_service():
subprocess.Popen(['systemctl', 'restart', 'exomy-camera-stream.service'])
def restart_exomy_container():
subprocess.Popen(['docker', 'restart', EXOMY_CONTAINER])
def reboot_raspberry_pi():
subprocess.Popen(['systemctl', 'reboot'])
def shutdown_raspberry_pi():
subprocess.Popen(['systemctl', 'poweroff'])
class Handler(http.server.BaseHTTPRequestHandler):
def end_headers(self):
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
super().end_headers()
def write_json(self, status_code, payload):
body = json.dumps(payload).encode('utf-8')
self.send_response(status_code)
self.send_header('Content-Type', 'application/json; charset=utf-8')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_OPTIONS(self):
self.send_response(204)
self.end_headers()
def do_GET(self):
if self.path == '/api/status':
self.write_json(200, collect_status())
return
self.write_json(404, {'status': 'Unbekannt'})
def do_POST(self):
actions = {
'/api/restart-camera': {
'handler': restart_camera_service,
'status': 'Kameradienst wird neu gestartet'
},
'/api/restart-container': {
'handler': restart_exomy_container,
'status': 'ExoMy-Container wird neu gestartet'
},
'/api/reboot': {
'handler': reboot_raspberry_pi,
'status': 'Raspberry Pi wird neu gestartet'
},
'/api/shutdown': {
'handler': shutdown_raspberry_pi,
'status': 'Raspberry Pi wird heruntergefahren'
}
}
action = actions.get(self.path)
if action is None:
self.write_json(404, {'status': 'Unbekannt'})
return
self.write_json(202, {'status': action['status']})
self.wfile.flush()
time.sleep(0.1)
action['handler']()
def log_message(self, fmt, *args):
return
if __name__ == '__main__':
with ReusableThreadingTCPServer(('0.0.0.0', PORT), Handler) as server:
server.daemon_threads = True
server.serve_forever()
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
import http.server
import socketserver
import subprocess
import threading
import time
BOUNDARY = b'--frame'
LATEST_JPEG = None
LATEST_LOCK = threading.Lock()
WIDTH = 1280
HEIGHT = 720
FPS = 10
PORT = 8081
def camera_reader():
global LATEST_JPEG
cmd = [
'rpicam-vid',
'--timeout', '0',
'--nopreview',
'--width', str(WIDTH),
'--height', str(HEIGHT),
'--framerate', str(FPS),
'--codec', 'mjpeg',
'-o', '-',
]
while True:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=0)
buffer = bytearray()
try:
while True:
chunk = proc.stdout.read(4096)
if not chunk:
break
buffer.extend(chunk)
while True:
start = buffer.find(b'\xff\xd8')
if start == -1:
if len(buffer) > 1024 * 1024:
del buffer[:-2]
break
end = buffer.find(b'\xff\xd9', start + 2)
if end == -1:
if start > 0:
del buffer[:start]
break
jpeg = bytes(buffer[start:end + 2])
del buffer[:end + 2]
with LATEST_LOCK:
LATEST_JPEG = jpeg
finally:
proc.kill()
proc.wait()
time.sleep(1)
class ReusableThreadingTCPServer(socketserver.ThreadingTCPServer):
allow_reuse_address = True
class Handler(http.server.BaseHTTPRequestHandler):
def do_HEAD(self):
if self.path in ('/', '/index.html'):
self.send_response(200)
self.send_header('Content-Type', 'text/html; charset=utf-8')
self.end_headers()
return
if self.path == '/stream.mjpg':
self.send_response(200)
self.send_header('Age', '0')
self.send_header('Cache-Control', 'no-cache, private')
self.send_header('Pragma', 'no-cache')
self.send_header('Content-Type', 'multipart/x-mixed-replace; boundary=frame')
self.end_headers()
return
self.send_error(404)
def do_GET(self):
if self.path in ('/', '/index.html'):
body = b'<html><body><img src="/stream.mjpg" /></body></html>'
self.send_response(200)
self.send_header('Content-Type', 'text/html; charset=utf-8')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
return
if self.path != '/stream.mjpg':
self.send_error(404)
return
self.send_response(200)
self.send_header('Age', '0')
self.send_header('Cache-Control', 'no-cache, private')
self.send_header('Pragma', 'no-cache')
self.send_header('Content-Type', 'multipart/x-mixed-replace; boundary=frame')
self.end_headers()
while True:
with LATEST_LOCK:
frame = LATEST_JPEG
if frame is None:
time.sleep(0.1)
continue
try:
self.wfile.write(BOUNDARY + b'\r\n')
self.wfile.write(b'Content-Type: image/jpeg\r\n')
self.wfile.write(f'Content-Length: {len(frame)}\r\n\r\n'.encode('ascii'))
self.wfile.write(frame)
self.wfile.write(b'\r\n')
self.wfile.flush()
time.sleep(1.0 / FPS)
except (BrokenPipeError, ConnectionResetError):
break
def log_message(self, fmt, *args):
return
if __name__ == '__main__':
threading.Thread(target=camera_reader, daemon=True).start()
with ReusableThreadingTCPServer(('0.0.0.0', PORT), Handler) as server:
server.daemon_threads = True
server.serve_forever()