Admin angepasst
This commit is contained in:
+275
-16
@@ -7,6 +7,7 @@ import http.server
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import socketserver
|
||||
import struct
|
||||
import subprocess
|
||||
@@ -85,6 +86,14 @@ def read_cpu_temperature():
|
||||
return 'unbekannt'
|
||||
|
||||
|
||||
def read_gpu_temperature():
|
||||
stdout, _, returncode = run_command(['vcgencmd', 'measure_temp'])
|
||||
if returncode != 0 or not stdout or '=' not in stdout:
|
||||
return 'unbekannt'
|
||||
value = stdout.split('=', 1)[1].strip().replace("'C", ' °C')
|
||||
return value or 'unbekannt'
|
||||
|
||||
|
||||
def read_cpu_usage():
|
||||
try:
|
||||
with open('/proc/stat', 'r', encoding='utf-8') as handle:
|
||||
@@ -108,6 +117,41 @@ def read_cpu_usage():
|
||||
return 'unbekannt'
|
||||
|
||||
|
||||
def read_per_core_usage():
|
||||
try:
|
||||
with open('/proc/stat', 'r', encoding='utf-8') as handle:
|
||||
first_lines = [line.split() for line in handle if line.startswith('cpu') and line[3:4].isdigit()]
|
||||
time.sleep(0.12)
|
||||
with open('/proc/stat', 'r', encoding='utf-8') as handle:
|
||||
second_lines = [line.split() for line in handle if line.startswith('cpu') and line[3:4].isdigit()]
|
||||
except (FileNotFoundError, ValueError):
|
||||
return 'unbekannt'
|
||||
|
||||
if len(first_lines) != len(second_lines) or not first_lines:
|
||||
return 'unbekannt'
|
||||
|
||||
results = []
|
||||
for first, second in zip(first_lines, second_lines):
|
||||
try:
|
||||
name = first[0]
|
||||
first_values = [int(value) for value in first[1:]]
|
||||
second_values = [int(value) for value in second[1:]]
|
||||
idle_first = first_values[3] + first_values[4]
|
||||
idle_second = second_values[3] + second_values[4]
|
||||
total_first = sum(first_values)
|
||||
total_second = sum(second_values)
|
||||
total_delta = total_second - total_first
|
||||
idle_delta = idle_second - idle_first
|
||||
if total_delta <= 0:
|
||||
continue
|
||||
usage = (1 - (idle_delta / total_delta)) * 100
|
||||
results.append(f'{name.upper()}: {usage:.0f}%')
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
|
||||
return ' | '.join(results) if results else 'unbekannt'
|
||||
|
||||
|
||||
def read_memory_usage():
|
||||
try:
|
||||
values = {}
|
||||
@@ -126,6 +170,27 @@ def read_memory_usage():
|
||||
return 'unbekannt'
|
||||
|
||||
|
||||
def read_memory_details():
|
||||
try:
|
||||
values = {}
|
||||
with open('/proc/meminfo', 'r', encoding='utf-8') as handle:
|
||||
for line in handle:
|
||||
key, raw_value = line.split(':', 1)
|
||||
values[key] = int(raw_value.strip().split()[0])
|
||||
total = values.get('MemTotal')
|
||||
available = values.get('MemAvailable')
|
||||
if not total or available is None:
|
||||
return 'unbekannt'
|
||||
used = total - available
|
||||
total_mb = total / 1024
|
||||
used_mb = used / 1024
|
||||
free_mb = available / 1024
|
||||
percent = (used / total) * 100
|
||||
return f'{used_mb:.0f} MB belegt / {free_mb:.0f} MB frei / {total_mb:.0f} MB gesamt ({percent:.0f}%)'
|
||||
except (FileNotFoundError, ValueError):
|
||||
return 'unbekannt'
|
||||
|
||||
|
||||
def read_undervoltage_status():
|
||||
stdout, _, returncode = run_command(['vcgencmd', 'get_throttled'])
|
||||
if returncode != 0 or not stdout or '=' not in stdout:
|
||||
@@ -161,6 +226,32 @@ def read_undervoltage_status():
|
||||
}
|
||||
|
||||
|
||||
def read_throttling_details():
|
||||
stdout, _, returncode = run_command(['vcgencmd', 'get_throttled'])
|
||||
if returncode != 0 or not stdout or '=' not in stdout:
|
||||
return 'unbekannt'
|
||||
|
||||
try:
|
||||
throttled_value = int(stdout.split('=', 1)[1].strip(), 16)
|
||||
except ValueError:
|
||||
return 'unbekannt'
|
||||
|
||||
flags = [
|
||||
(0x1, 'Unterspannung aktuell'),
|
||||
(0x2, 'ARM gedrosselt aktuell'),
|
||||
(0x4, 'aktuell limitiert'),
|
||||
(0x8, 'Soft-Temperaturlimit aktuell'),
|
||||
(0x10000, 'Unterspannung früher'),
|
||||
(0x20000, 'ARM früher gedrosselt'),
|
||||
(0x40000, 'früher limitiert'),
|
||||
(0x80000, 'Soft-Temperaturlimit früher'),
|
||||
]
|
||||
active = [label for bitmask, label in flags if throttled_value & bitmask]
|
||||
if not active:
|
||||
return 'keine Auffälligkeiten'
|
||||
return ' | '.join(active)
|
||||
|
||||
|
||||
def format_uptime():
|
||||
try:
|
||||
with open('/proc/uptime', 'r', encoding='utf-8') as handle:
|
||||
@@ -188,6 +279,39 @@ def format_disk_free():
|
||||
return f'{free_gb:.1f} GB frei / {total_gb:.1f} GB'
|
||||
|
||||
|
||||
def format_path_disk_usage(path):
|
||||
try:
|
||||
usage = shutil.disk_usage(path)
|
||||
free_gb = usage.free / (1024 ** 3)
|
||||
used_gb = usage.used / (1024 ** 3)
|
||||
total_gb = usage.total / (1024 ** 3)
|
||||
return f'{used_gb:.1f} GB belegt / {free_gb:.1f} GB frei / {total_gb:.1f} GB'
|
||||
except FileNotFoundError:
|
||||
return 'unbekannt'
|
||||
|
||||
|
||||
def read_inode_free(path='/'):
|
||||
try:
|
||||
stats = os.statvfs(path)
|
||||
total = stats.f_files
|
||||
free = stats.f_ffree
|
||||
used = total - free
|
||||
if total <= 0:
|
||||
return 'unbekannt'
|
||||
percent_used = (used / total) * 100
|
||||
return f'{free:,} frei / {total:,} gesamt ({percent_used:.0f}% belegt)'.replace(',', '.')
|
||||
except OSError:
|
||||
return 'unbekannt'
|
||||
|
||||
|
||||
def read_load_average():
|
||||
try:
|
||||
load_1, load_5, load_15 = os.getloadavg()
|
||||
return f'{load_1:.2f} / {load_5:.2f} / {load_15:.2f}'
|
||||
except (AttributeError, OSError):
|
||||
return 'unbekannt'
|
||||
|
||||
|
||||
def get_ip_addresses():
|
||||
stdout, _, returncode = run_command(['ip', '-4', '-o', 'addr', 'show', 'dev', 'wlan0', 'scope', 'global'])
|
||||
if returncode == 0 and stdout:
|
||||
@@ -235,29 +359,87 @@ def get_wifi_status():
|
||||
return 'kein wlan0'
|
||||
|
||||
|
||||
def get_wifi_signal():
|
||||
stdout, _, returncode = run_command(['nmcli', '-t', '-f', 'GENERAL.STATE,AP.SIGNAL', 'device', 'show', 'wlan0'])
|
||||
def get_wifi_details():
|
||||
details = {
|
||||
'ssid': 'unbekannt',
|
||||
'signal_percent': 'unbekannt',
|
||||
'signal_dbm': 'unbekannt',
|
||||
'channel': 'unbekannt',
|
||||
'frequency': 'unbekannt',
|
||||
}
|
||||
stdout, _, returncode = run_command(['nmcli', '-t', '-f', 'ACTIVE,SSID,SIGNAL,CHAN,FREQ,BARS', 'device', 'wifi', 'list', 'ifname', 'wlan0'])
|
||||
if returncode != 0 or not stdout:
|
||||
return 'unbekannt'
|
||||
return details
|
||||
|
||||
state = ''
|
||||
signal = ''
|
||||
for line in stdout.splitlines():
|
||||
if line.startswith('GENERAL.STATE:'):
|
||||
state = line.split(':', 1)[1].strip()
|
||||
elif line.startswith('AP[1].SIGNAL:'):
|
||||
signal = line.split(':', 1)[1].strip()
|
||||
if not line.startswith('yes:'):
|
||||
continue
|
||||
parts = line.split(':')
|
||||
if len(parts) >= 6:
|
||||
ssid = parts[1].strip()
|
||||
signal = parts[2].strip()
|
||||
channel = parts[3].strip()
|
||||
frequency = parts[4].strip()
|
||||
details['ssid'] = ssid or 'unbekannt'
|
||||
details['signal_percent'] = f'{signal} %' if signal else 'unbekannt'
|
||||
details['channel'] = channel or 'unbekannt'
|
||||
details['frequency'] = frequency or 'unbekannt'
|
||||
try:
|
||||
dbm = (int(float(signal)) / 2) - 100
|
||||
details['signal_dbm'] = f'{dbm:.0f} dBm'
|
||||
except ValueError:
|
||||
details['signal_dbm'] = 'unbekannt'
|
||||
break
|
||||
return details
|
||||
|
||||
if 'connected' not in state.lower():
|
||||
return 'nicht verbunden'
|
||||
if not signal:
|
||||
return 'unbekannt'
|
||||
|
||||
def get_wifi_signal():
|
||||
details = get_wifi_details()
|
||||
return details['signal_percent']
|
||||
|
||||
|
||||
def get_hostname():
|
||||
try:
|
||||
percent = max(0, min(100, int(float(signal))))
|
||||
except ValueError:
|
||||
return socket.gethostname()
|
||||
except Exception:
|
||||
return 'unbekannt'
|
||||
return f'{percent} %'
|
||||
|
||||
|
||||
def get_kernel_version():
|
||||
stdout, _, returncode = run_command(['uname', '-r'])
|
||||
if returncode == 0 and stdout:
|
||||
return stdout
|
||||
return 'unbekannt'
|
||||
|
||||
|
||||
def get_pi_model():
|
||||
try:
|
||||
with open('/proc/device-tree/model', 'r', encoding='utf-8') as handle:
|
||||
return handle.read().strip('\x00').strip() or 'unbekannt'
|
||||
except (FileNotFoundError, ValueError):
|
||||
return 'unbekannt'
|
||||
|
||||
|
||||
def get_system_time_details():
|
||||
local_time = datetime.datetime.now().astimezone()
|
||||
timedatectl_stdout, _, timedatectl_returncode = run_command(['timedatectl', 'show', '-p', 'NTPSynchronized', '-p', 'SystemClockSynchronized', '-p', 'Timezone'])
|
||||
ntp_sync = 'unbekannt'
|
||||
clock_sync = 'unbekannt'
|
||||
timezone_name = local_time.tzname() or 'unbekannt'
|
||||
if timedatectl_returncode == 0 and timedatectl_stdout:
|
||||
for line in timedatectl_stdout.splitlines():
|
||||
if line.startswith('NTPSynchronized='):
|
||||
ntp_sync = 'ja' if line.split('=', 1)[1].strip().lower() == 'yes' else 'nein'
|
||||
elif line.startswith('SystemClockSynchronized='):
|
||||
clock_sync = 'ja' if line.split('=', 1)[1].strip().lower() == 'yes' else 'nein'
|
||||
elif line.startswith('Timezone='):
|
||||
timezone_name = line.split('=', 1)[1].strip() or timezone_name
|
||||
return {
|
||||
'local': local_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'timezone': timezone_name,
|
||||
'ntp_sync': ntp_sync,
|
||||
'clock_sync': clock_sync,
|
||||
}
|
||||
|
||||
|
||||
def get_systemd_state(service_name):
|
||||
@@ -274,6 +456,59 @@ def get_container_state(container_name):
|
||||
return 'unknown'
|
||||
|
||||
|
||||
def get_container_details(container_name):
|
||||
stdout, _, returncode = run_command(['docker', 'inspect', container_name])
|
||||
if returncode != 0 or not stdout:
|
||||
return {
|
||||
'uptime': 'unbekannt',
|
||||
'restarts': 'unbekannt',
|
||||
'docker_disk': 'unbekannt',
|
||||
}
|
||||
|
||||
try:
|
||||
payload = json.loads(stdout)
|
||||
info = payload[0] if payload else {}
|
||||
state = info.get('State', {})
|
||||
started_at = state.get('StartedAt')
|
||||
restart_count = info.get('RestartCount', state.get('RestartCount', 'unbekannt'))
|
||||
uptime = 'unbekannt'
|
||||
if started_at and started_at != '0001-01-01T00:00:00Z':
|
||||
started = datetime.datetime.fromisoformat(started_at.replace('Z', '+00:00'))
|
||||
delta = datetime.datetime.now(datetime.timezone.utc) - started
|
||||
uptime = format_duration(int(delta.total_seconds()))
|
||||
return {
|
||||
'uptime': uptime,
|
||||
'restarts': str(restart_count),
|
||||
'docker_disk': get_docker_disk_usage(),
|
||||
}
|
||||
except (ValueError, KeyError, TypeError, IndexError):
|
||||
return {
|
||||
'uptime': 'unbekannt',
|
||||
'restarts': 'unbekannt',
|
||||
'docker_disk': 'unbekannt',
|
||||
}
|
||||
|
||||
|
||||
def get_docker_disk_usage():
|
||||
stdout, _, returncode = run_command(['docker', 'info', '-f', '{{.DockerRootDir}}'])
|
||||
if returncode != 0 or not stdout:
|
||||
return 'unbekannt'
|
||||
return format_path_disk_usage(stdout.strip())
|
||||
|
||||
|
||||
def format_duration(seconds):
|
||||
days, remainder = divmod(max(0, int(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 get_motor_test_container_status():
|
||||
state = get_container_state(EXOMY_CONTAINER)
|
||||
return {
|
||||
@@ -285,25 +520,49 @@ def get_motor_test_container_status():
|
||||
|
||||
def collect_status():
|
||||
undervoltage = read_undervoltage_status()
|
||||
wifi_details = get_wifi_details()
|
||||
time_details = get_system_time_details()
|
||||
container_details = get_container_details(EXOMY_CONTAINER)
|
||||
return {
|
||||
'status': 'Bereit',
|
||||
'system': {
|
||||
'wifi': get_wifi_status(),
|
||||
'wifi_signal': get_wifi_signal(),
|
||||
'wifi_signal_dbm': wifi_details['signal_dbm'],
|
||||
'wifi_ssid': wifi_details['ssid'],
|
||||
'wifi_channel': wifi_details['channel'],
|
||||
'wifi_frequency': wifi_details['frequency'],
|
||||
'ips': get_ip_addresses(),
|
||||
'undervoltage': undervoltage['text'],
|
||||
'undervoltage_state': undervoltage['state'],
|
||||
'throttling_details': read_throttling_details(),
|
||||
'cpu_temperature': read_cpu_temperature(),
|
||||
'gpu_temperature': read_gpu_temperature(),
|
||||
'cpu_usage': read_cpu_usage(),
|
||||
'cpu_usage_per_core': read_per_core_usage(),
|
||||
'memory_usage': read_memory_usage(),
|
||||
'memory_details': read_memory_details(),
|
||||
'uptime': format_uptime(),
|
||||
'disk_free': format_disk_free(),
|
||||
'disk_root': format_path_disk_usage('/'),
|
||||
'disk_docker': container_details['docker_disk'],
|
||||
'inode_free': read_inode_free('/'),
|
||||
'load_average': read_load_average(),
|
||||
'hostname': get_hostname(),
|
||||
'kernel_version': get_kernel_version(),
|
||||
'pi_model': get_pi_model(),
|
||||
'system_time': time_details['local'],
|
||||
'system_timezone': time_details['timezone'],
|
||||
'ntp_sync': time_details['ntp_sync'],
|
||||
'clock_sync': time_details['clock_sync'],
|
||||
},
|
||||
'services': {
|
||||
'camera': get_systemd_state('exomy-camera-stream.service'),
|
||||
'camera_settings_version': read_camera_settings_version(),
|
||||
'admin_api': get_systemd_state('exomy-admin-api.service'),
|
||||
'exomy': get_container_state(EXOMY_CONTAINER),
|
||||
'exomy_uptime': container_details['uptime'],
|
||||
'exomy_restarts': container_details['restarts'],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user